If you have a limited set of libraries (like A) you want to support, I’d just have B.ofA(...) and be done with it. - obviously, this is nit loosely coupled.
You could further use functors (or even first class modules) and module types (depending on, what A is actually doing):
Then your library B would have a functor (function, which takes one or more modules as it’s arguments and returns another module).
module B = {
module type A = {
type t
let getSomeProp: t => string
}
module type B = {
type t
let doX: t => int
}
module Make = (A: A): (B with type t = A.t) => {
type t = A.t
let calculateSomeInt = s => s->Js.String.length
let doX = t => t->A.getSomeProp->calculateSomeInt
}
}
// Usage
module SpecificA = {
type t = {
name: string,
age: int,
}
let getSomeProp = t => t.name
let make = (~name: string, ~age: int) => {name, age}
}
module SpecificB = B.Make(SpecificA)
let a = SpecificA.make(~name="Alice", ~age=42)
let x = SpecificB.doX(a)
I’m on the go and just typing on my mobile, I’ll try to find some time later during the day to give another example using first class modules. (Which might fit your use case a little bit better…)
module B = {
module type A = {
type t
let getSomeProp: t => string
}
let calculateSomeInt = s => s->Js.String.length
let doX:
type a. (a, module(A with type t = a)) => int =
(t, module(A)) => t->A.getSomeProp->calculateSomeInt
}
/* Usage */
module SpecificA = {
type t = {
name: string,
age: int,
}
let getSomeProp = t => t.name
let make = (~name: string, ~age: int) => {name, age}
}
let a = SpecificA.make(~name="Alice", ~age=42)
let x = B.doX(a, module(SpecificA))
To add to the answers above, it’s worth mentioning that you actually do use structural typing between third-party modules, since module types are structural.
Module types are almost analogous to structurally-typed OOP code. If you can define what type of interface you need, then you just need a module that fits that type.
module type Stringable = {
type t
let toString: t => string
}
module type Mappable = {
type t<'a>
let make: 'a => t<'a>
let map: (t<'a>, 'a => 'b) => t<'b>
let flatMap: (t<'a>, 'a => t<'b>) => t<'b>
}
module type Encodeable = {
type t
let toJson: t => Js.Json.t
let fromJson: Js.Json.t => t
}
module type HasName = {
type t
let firstName: t => string
let lastName: t => string
}
// ...etc
Once you define the interface (module type) you need, you use can a functor or first-class-module that accepts it. Any third-party module that conforms to that signature will work. The general idea is to think about interfaces rather than implementations.
In cases where it makes sense to expose the type implementation, you can always use a polymorphic variant or object type, since those are also structural.