How to achieve loose coupling between third party modules?

Given

module ThirdPartyA = {
  type t = {..........}
  let make = () => t
}

And

module ThirdPartyB = {
  let do = (x:.........) => {
    // do something with x
  }
}

finally

let a = ThirdPartyA.make()
ThirdPartyB.do(a)

Are there strategies in rescript for B to use a type A.t?

In typescript, go and other structurally types languages, it is a no brainer (and gophers wont stop gushing about it), and I think rust uses traits.

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).

Code Example in Playground

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…)

3 Likes

So here’s the promised example using first class modules:

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))
1 Like

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.

3 Likes