5.3 Module Interfaces
July 10, 2026 ยท View on GitHub
Module Interfaces
The primary use of interfaces is to define a general pattern to describe a Hardcaml
module. We will specify two interfaces called I and O which represent the input and
output signals of a module along with a function called create to generate the logic.
module I : Interface.S
module O : Interface.S
val create : Signal.t I.t -> Signal.t O.t
Hardcaml provides various functors (usually called With_interface) that will generate
circuits, simulators, hierarchical instantiations, and more if we follow this pattern.
๐ The type of the create function does not have to precisely follow this signature. It is fine to have further arguments (i.e. some configuration parameters) so long as it ends with
Signal.t I.t -> Signal.t O.t.
Example
module I = struct
type 'a t =
{ clock : 'a
; d : 'a[@bits 8]
}
[@@deriving hardcaml]
end
module O = struct
type 'a t =
{ q : 'a[@bits 8]
; q_n : 'a[@bits 8]
}
[@@deriving hardcaml]
end
# let create (i : _ I.t) =
let q = reg (Reg_spec.create ~clock:i.clock ()) i.d in
{ O.q; q_n = ~: q }
val create : t I.t -> t O.t = <fun>
With_interface
Hardcaml supports this pattern by providing a number of functors usually called
With_interface. They take the I and O interfaces as parameters and will provide a
function which takes create.
For example, we can create a circuit as follows:
# module My_circuit = Circuit.With_interface(I)(O)
module My_circuit :
sig
type create = Hardcaml_kernel.Interface.Create_fn(I)(O).t
val create_exn :
?config:Circuit.Config.t ->
?input_attributes:Rtl_attribute.t list I.t ->
?output_attributes:Rtl_attribute.t list O.t ->
name:string -> create -> Circuit.t
end
# My_circuit.create_exn ~name:"reg" create
- : Circuit.t = <abstr>
In contrast this is what we would write without interfaces:
# let create2 ~clock ~d =
let q = reg (Reg_spec.create ~clock ()) d in
q, ~:q
val create2 : clock:t -> d:t -> t * t = <fun>
# let q, q_n = create2 ~clock:(input "clock" 1) ~d:(input "d" 8)
val q : t =
(register (width 8) ((clock clock) (clock_edge Rising)) (data_in d))
val q_n : t = (not (width 8) (arguments (register)))
# Circuit.create_exn ~name:"reg"
[ output "q" q; output "q_n" q_n ]
- : Circuit.t = <abstr>
As the number of signals into and out of a module grows (and 100's or even 1000's is not infeasible) the utility of interfaces to deal with boilerplate grows.
Configuration
When we define an interface we specify things like bit widths, field existence (using options) and array or list lengths. However, we often want to write something more generic. To do this we can use functors.
Taking the previous example, lets make the register width configurable.
module Make(Config : sig val register_width : int end) = struct
module I = struct
type 'a t =
{ clock : 'a
; d : 'a[@bits Config.register_width]
}
[@@deriving hardcaml]
end
module O = struct
type 'a t =
{ q : 'a[@bits Config.register_width]
; q_n : 'a[@bits Config.register_width]
}
[@@deriving hardcaml]
end
let create (i : _ I.t) =
let q = reg (Reg_spec.create ~clock:i.clock ()) i.d in
{ O.q; q_n = ~: q }
end
We can now instantiate the functor and generate a circuit as before.
module My_design = Make(struct let register_width = 4 end)
module My_circuit2 = Circuit.With_interface(My_design.I)(My_design.O)
# My_circuit2.create_exn ~name:"reg" My_design.create
- : Circuit.t = <abstr>
Interface type (in-)equality
Consider the following 2 instantiations of our design.
module My_design16 = Make(struct let register_width = 16 end)
module My_design32 = Make(struct let register_width = 32 end)
Inside each instantiation we have records of type I.t and O.t. They have exactly the
same definition so are the types equal?
No. Because they are created inside a functor, distinct types are generated.
On the other hand would we like them to be equal? I would argue no. Because they represent interfaces with different bitwidths I would like the type system to differentiate them.
On occasion it is useful to expose the type equality and it can still be done.
module X = struct
type 'a t = { x : 'a }[@@deriving hardcaml]
end
module Make_X(Config : sig val width : int end)
: Interface.S with type 'a t = 'a X.t
= struct
include Interface.Update(X)(struct
let port_names_and_widths = { X.x = "x", Config.width }
end)
end
Interface.Update takes an existing interface and redefines the names and bitwidths of
the fields. The type constraint with type 'a t = 'a X.t exposes the desired equality.
module Y = Make_X(struct let width = 10 end)
module Z = Make_X(struct let width = 20 end)
As shown below the types of Y.t and Z.t are compatible.
# [ Y.port_names; Z.port_names ]
- : string Z.t list = [{X.x = "x"}; {X.x = "x"}]