Module functors vs. records-of-functions for interface/implementation separation

i tend to always do records-of-functions because its easier to pass around, but it might because i don’t know something about rescript.

see the two examples:

module Example1 = {
  module type Printer = {
    let println: string => unit
  }
  
  module ConsolePrinter: Printer = {
    let println = Console.log
  }
  
  
  module SomeModule = (P: Printer) => {
    let doSomething = () => {
      P.println("hello")
    }
  }
  
  let main = () => {
    module SM = SomeModule(ConsolePrinter)
    SM.doSomething()
  }
}
module Example2 = {
  module Printer = {
    type t = {
      println: string => unit
    }
  }
  
  module ConsolePrinter = {
    let make = (): Printer.t => {
      { println: Console.log }
    }
  }
  
  module SomeModule = {
    type t = {
      printer: Printer.t
    }
    
    let make = (~printer) => {
      { printer: printer }
    }
    
    let doSomething = (sm) => {
      sm.printer.println("hello")
    }
  }
  
  let main = () => {
    let sm = SomeModule.make(~printer=ConsolePrinter.make())
    sm->SomeModule.doSomething
  }
}

when do i want to use one or the other? is there a preferred convention in rescript? can we pass modules around as first-class objects easily in e.g. a record or type. how?

get back with your thoughts

I think passing records of functions is quite uncommon in ReScript. The more idiomatic convention would be to use modules or even just pass the function as a parameter:

module Example3 = {
 
  module SomeModule = {
    let doSomething = (~println) => {
      println("hello")
    }
  }
  
  let main = () => {
    SomeModule.doSomething(~println=Console.log)
  }
}

You can actually pass modules around using “first-class modules”, but I don’t think it’s documented today since the use cases are quite uncommon. Example:

module type Foo = {
  let print: unit => unit
}

let bar = (foo: module(Foo)) => {
  let module(Foo) = foo
  Foo.print()
}

module Foo = {
  let print = () => Console.log("hello")
}

bar(module(Foo))

thanks, i realize that you can pass around functions but if we’re building a dependency root which has services that model behaviors, they usually come as “bunch of functions” rather than a single one (e.g., imagine a repository, which will likely always have a get and a set built on the same persistence layer).

i think my question is more along the lines of “how do you provide and pass around interfaces” (think hexagonal architecture)

then module functions (or first class modules) is the way to go.

1 Like

do i understand you correctly, you’d suggest something like this?

// Clock.res
module type T = {
  let now: unit => float
}

// SystemClock.res
let now = () => {
  Date.make()->Date.getTime->Float.toInt
}

// Deps.res (composition root)
type t = {clock: module(Clock.T)}

let make = () => {
  let clock = module(SystemClock: Clock.T)

  {clock: clock}
}

just as an example. am i on the right track?

toying around with first-class modules, i’m doing this:

type t = {
  clock: module(Clock.T),
  getLog: string => module(Logger.T),
}

let make = () => {
  module C = SystemClock

  let getLog = (name) => module(ConsoleLogger.Make(C, {let name = name}) : Logger.T)

  {clock: module(C), getLog}
}

it sort of makes more sense, but it’s also a bit clunky. anyway, funnily enough, the rescript formatter that i brought up in the other thread absolutely mangles it:

type t = {
  clock: module(Clock.T),
  getLog: string => module(Logger.T),
}

let make = () => {
  module C = SystemClock

  let getLog = (name): module(Logger.T) =>
    module(
      ConsoleLogger.Make(
        C,
        {
          let name = name
        },
      )
    )

  {clock: module(C), getLog}
}

do i just have to live with this or is there anything i can do? (it even added the return type annotation!)

1 Like

also, is there a difference between these two:

  module Log = unpack(deps.getLog("foo"))
  let module(Log) = deps.getLog("foo")

do i prefer one or the other. or do they have different uses?

I think I’d stick to module functions for such use cases, I’m not sure first-class modules and function records bring any benefit here. In general try to use the least powerful tool for the job.

It’s hard to judge without having the big picture of what you’re actually trying to do, but I’m pretty sure you could use a much simpler setup.

Simply passing functions instead of records of functions returning first-class modules make things much simpler to read and allows to know exactly the dependencies a given function has. If you’re not cautious, you could end passing around a huge “dependency root” you’d only use a few percents of in a given function and I think it’d then be pretty hard to tree-shake.

maybe i’m not being clear. there are two main concepts i’m discussing here: interfaces (think ports and adapters) and a composition root (think dependency injection)

i understand your point on injecting functions as needed, but that doesnt really make sense for larger applications where functions are often bundled together to model a behavior (i.e., log info/debug/warn/error or even more so, a repository with getbyid, getall, save, modify etc).

the way i usually do it is a sort of “hexagonal domain-driven architecture” and if we stick to the ports/adapters part of that, i do ports (e.g. Clock.t = {now:unit=>float}) and adapters (e.g. SystemClock which has now=()=>Date.now)

so the question here is, what is preferrable to model interfaces (think interface IClock in typescript):

A:

// Clock.res
type t = {
  now: unit => float
}

// SystemClock.res (i understand `make` isnt right for SystemClock in particular as its a singleton instance, but ignore that for now)
let make = (): Clock.t =  {
    {now: () => Date.now()}
}

or B:

// Clock.res
module type T =  {
  let now: unit => float
}

// SystemClock.res
let now = () => Date.now()

so this is the first question, whats the better way to model interfaces in rescript.

the second question tied into the first is what’s better considering we’re doing dependency injection starting at the composition root, e.g. Deps.res, where we wire everything up:

A:

// Deps.res
type t = {
  clock: Clock.t
}

let make = () => {
  let clock = SystemClock.make()
  { clock: clock }
}

or B:

// Deps.res
type t =  {
  clock: module(Clock.T)
}

let make = () => {
  module ClockImpl = SystemClock

  { clock: module(ClockImpl) }
}

so this question is, with dependency injection in mind, do we prefer A or B?

you might also suggest another alternative, i.e. injecting functions directly, but to me, that only makes sense for trivial cases. instead, imagine a larger port/interface with maybe 5 different functions that together model a behavior (again, think repository, get, set, search, getall, modifu, overwrite, whatever, these functions will be coupled together as they all work with the same persistence layer, so should not be passed around individually?)

third, my question is what’s the difference between these two and what are their uses and when do i prefer whta:

A:

module Log = unpack(deps.getLog("foo"))

or B:

let module(Log) = deps.getLog("foo")

trying to really wrap my head around the right way to think in rescript terms here

I tried the hexagonal / ports and adapters architecture in a rescript app as well. I had the same questions in mind. Should I pass single functions, records of functions or modules. Since I didn’t have to define different types for different implementations, module functions were a bit of an overhead. The verbose syntax bothered me.

As always: Use the solution most convenient to you.


I would like to see more articles like Full-stack ReScript. Architecture Overview | Full Steak Dev, where someone explains how he / she structures their applications.

2 Likes

thanks. I’m experimenting a bit. we do records-of-fns now, or capability records, because it’s ergonomic. but it always bothered me to have type t for behaviors (in essence, modules, or in hex-ddd terms, ports) instead of module type T. to me, t represents value types (domain).

i’ll keep toying with this for a bit, but i’d also like to see more articles on it. i tried looking into ocaml, but the “enterprisey” setup for rather larger applications with a composition root and testing (which is where hex ddd shines), the problem seems rather “unsolved” in the sense that there is set convention for how to accomplish it in rescript (nor ocaml!?). curious. i might update this thread with toy examples just to compare different ways of doing it.

1 Like

I think it’s not a by chance that there’s no convention and few articles about that in rescript/ocaml or functional languages in general. A big motivation of dependency injection comes from the characteristics of Object Oriented Programming itself.

In OOP the base brick is objects/classes, you have side effects and local states everywhere, you need dependency injection to mock the local states and side effects and test things in a given set of local states. In functional programming, most things are immutable and side-effects are much less common. You can easily test things in isolation and if some functions are pure and tested, you can fully trust their combination. Try to stick side-effectful functions to the edges, test them with simple dependency injection (simple function passing is usually enough, but use some first-class modules if you absolutely need to, I think it’s a bit more idiomatic than records of functions then), then write pure (no side-effects) functions you can test easily without dependency injection.

Trying hard to replicate things that come from other paradigms is usually not a great idea, you try to solve problems that mostly don’t even exist, so why even bother? My advice with functional programming is usually to follow the basic conventions (gather functions in modules around their main type t) and see how far it gets you. I’ve been working with OCaml/rescript as my main languages at work for the past 5 years and never really felt the need for more complex architectures. You can find some huge OCaml projects that are 25 years old and still use very simple architectures and basic concepts in general.

1 Like

if you really need to pass around some dependencies, I’d likely use something like that:

module DepsTypes = {
  module type Console = {
    let log: string => unit
  }

  module type T = {
    module Console: Console
  }
}

module Deps: DepsTypes.T = {
  module Console: DepsTypes.Console = Console
}

module File1 = {
  let bar = (a, b, ~deps=module(Deps: DepsTypes.T)) => {
    let module(Deps) = deps
    Deps.Console.log(Int.toString(a + b))
  }
}

File1.bar(1, 2)

module TestFile1 = {
  module Console = {
    let logs = ref("")
    let log = s => {logs := logs.contents + s}
    let flushLog = () => {logs := ""}
  }
  let deps = module(
    {
      module Console = Console
    }: DepsTypes.T
  )

  let testBar = {
    Console.flushLog()
    File1.bar(1, 2, ~deps)
    assertEqual(Console.logs.contents, "3")
  }
}

But even in such cases, I’d likely rather pass only the dependencies needed instead of a dependency root:

module DepsTypes = {
  module type Console = {
    let log: string => unit
  }

  module type T = {
    module Console: Console
  }
}

module Deps: DepsTypes.T = {
  module Console: DepsTypes.Console = Console
}

module File1 = {
  let bar = (a, b, ~console=module(Deps.Console: DepsTypes.Console)) => {
    let module(Console) = console
    Console.log(Int.toString(a + b))
  }
}

File1.bar(1, 2)

module TestFile1 = {
  module Console = {
    let logs = ref("")
    let log = s => {logs := logs.contents + s}
    let flushLog = () => {logs := ""}
  }
  let console = module(Console: DepsTypes.Console)

  let testBar = {
    Console.flushLog()
    File1.bar(1, 2, ~console)
    assertEqual(Console.logs.contents, "3")
  }
}
1 Like

thanks, interesting input. I agree with you in general terms, but in real-world scenarios, especially for larger production systems, the “edges” are everywhere, you have side-effects all over the place, not necessarily in terms of internal (with respect to the app itself) states, but rather the world around it. db reads/writes/searches, logging to different outputs under different names, file upload/storage/download, user-interactions, authentication states, app configurations that sometimes needs to change for parts of the app.

if you have e.g. an api endpoint that needs to log, store a file, find a file, genreate ids and what have you, you’ll end up injecting 20 different functions instead of maybe 3 or 4 dependency objects (e.g. capability records). i don’t see how this is solved by things be internally immutable, because the world around the app sure as heck isn’t - and after all, if we don’t interact heavily with it, we can’t do anything useful at all.

if you grasp the concept of a composition root with dependency injection (coming from the OO paradigm, lets say), could you provide a relevant example of how it would look in terms of files/injections in rescript? keep in mind you still want to wire things up in one single place. i’d like to wrap my head around it as i feel that i currently cannot, but i’m also rather sure it’s mainly because of my lack of competence in the fp domain, but i’m also more and more sure that there aren’t any good real-world conventions yet (most of the ones i’ve seen are academic and/or contrived)

experimenting with this, i threw this contrived but i think relevant example together:


// src/Deps.res

module Clock = SystemClock
module BaseLog = TimestampedLog.Make(Clock, ConsoleLog, SecsSinceStartTimestampedLogFmt)

let mkLog = (name): module(Log.T) =>
  module(
    NamedLog.Make(
      BaseLog,
      BracketedNamedLogFmt,
      {
        let name = name
      },
    )
  )

module FileRepo = InMemoryFileRepo.Make(Clock, unpack(mkLog("bosse")))

// src/DoSomething.res

module Make = (Clock: Clock.T, Log: Log.T) => {
  let foo = () => {
    let now = Clock.now()->Float.toString
    Log.info(`now is ${now}`)
  }
}

// src/Main.res

let main = () => {
  module DS = DoSomething.Make(Deps.Clock, unpack(Deps.mkLog("DoSomething")))
  DS.foo()
}

main()

// src/_Adapters/BracketedNamedLogFmt.res

let fmt = (~name, ~text) => {
  `[${name}] ${text}`
}

// src/_Adapters/ConsoleLog.res

let info = Console.log

// src/_Adapters/InMemoryFileRepo.res

module Make = (Clock: Clock.T, Log: Log.T) => {
  let store = dict{}

  let getById = async id => {
    let now = Clock.now()->Float.toString
    Log.info(`looking for file: ${id->Id.toString} now is ${now}`)
    store->Dict.get(id->Id.toString)
  }

  let save = async (file: File.t) => {
    store->Dict.set(file.id->Id.toString, file)
  }
}

// src/_Adapters/NamedLog.res

module Fmt = {
  module type T = {
    let fmt: (~name: string, ~text: string) => string
  }
}

module Make = (
  Log: Log.T,
  Fmt: Fmt.T,
  Conf: {
    let name: string
  },
) => {
  let info = s => {
    Log.info(Fmt.fmt(~name=Conf.name, ~text=s))
  }
}

// src/_Adapters/SecsSinceStartTimestampedLogFmt.res

let start = Date.now()

let fmt = (~ts, ~text) => {
  let s = (ts - start) / 1000.0
  `(${s->Float.toString}s) ${text}`
}

// src/_Adapters/SystemClock.res

let now = () => {
  Date.now()
}

// src/_Adapters/TimestampedLog.res

module Fmt = {
  module type T = {
    let fmt: (~ts: float, ~text: string) => string
  }
}

module Make = (Clock: Clock.T, Log: Log.T, Fmt: Fmt.T) => {
  let info = text => {
    let ts = Clock.now()
    Log.info(Fmt.fmt(~ts, ~text))
  }
}

// src/_Domain/File.res

type t = {
  id: Id.t,
  name: string,
}

let make = (~id, ~name) => {
  id,
  name,
}

// src/_Domain/Id.res

type t = string

let fromString = s => s
let toString = s => s

// src/_Ports/Clock.res

module type T = {
  let now: unit => float
}

// src/_Ports/FileRepo.res

module type T = {
  let getById: Id.t => promise<option<File.t>>
  let save: File.t => promise<unit>
}

// src/_Ports/Log.res

module type T = {
  let info: string => unit
}

it does get sort of verbose and i could inline a few things here and there. i dont like using unpack however (it seems to be entirely undocumented)

also @tsnobip your input welcome if i’m doing something crazy here

1 Like

So at CCA we are mostly using implementation records for dependency injection.

Don’t know if it’s idiomatic ReScript, but it does not need any concepts that JS devs are unfamiliar with.

Here is a realworld example for such a module

// PlatformFunctions.res
type impl = {keepAwakeWhile: 'a. (string, unit => promise<'a>) => promise<'a>}

let defaultImpl: impl = {
  keepAwakeWhile: (_, promiseCreator) => promiseCreator(),
}

let impl = ref(defaultImpl)

let setImpl = theImpl => impl := theImpl

let keepAwakeWhile = (name, promiseCreator) => impl.contents.keepAwakeWhile(name, promiseCreator)

// IOSPlatformFunctions.res
let impl: PlatformFunctions.impl = {
  keepAwakeWhile: async (name, promiseCreator) => { ... }

Then (usually on app startup) we set the impl:

PlatformFunctions.setImpl(IOSPlatformFunctions.impl)

and anywhere in the app then call it:

await PlatformFunctions.keepAwakeWhile("confirmPushReceived", onPushReceived)

Previously we used first-class modules for that but it turned out to be overkill when we found out about Scoped Polymorphic Types | ReScript Language Manual.

2 Likes

Sure the real world is impure, but it doesn’t mean your whole code has to be! The Clean Architecture that reuses most of the principles of the hexagonal architecture recommends to keep your interactions with the real world in the outer layers, on the outskirts of your architecture.

So for the outer layers, you can use some kind of dependency injection, the one @fham suggested is quite simple and elegant, but separating functions that interact with the real world from pure functions is a good practice, to avoid “leaking” the real world everywhere.

it’s what i’ve been doing too. you’re using a static impl though which is sort of a no-no to me but it makes sense just from the name (you likely won’t be running your app instance on more than one platform at a time :smile:)

i’m exploring using module type T to model behaviors instead of type t though and it actually sort of makes sense even though i’m not necessarily generating actual types. in that sense i tend to agree with @tsnobip earlier comment re. records-of-fns being at least uncommon for this purpose in rescript. i haven’t landed on anything final though so just exploring ideas here.

ps. what’s CCA?

absolutely agree with this take, but my reasoning focuses on the particular areas where there is interaction (hence Clock and Log above in the examples). i’m actually leaning towards module functions now despite somewhat heavier syntax.

At least somewhat related, but you can also use ReScript objects if you want to leverage full inference for DI. Here’s an example that uses %typeof, which does not exist yet (but there’s a PoC of):

// The installed NodeJs bindings
module Node = {
  module Fs = {
    @module("node:fs")
    external readFileSync: (string, string) => string = "readFileSync"
  }
}

// This is our fn that uses DI via the `ctx` arg
let processFile = (filename, ~ctx) => {
  let readfile: %typeof(Node.Fs.readFileSync) = ctx["readfile"]
  let content = readfile(filename, "utf8")
  content->String.split("\n")->Array.map(line => line->String.trim)
}

// Inject the real thing in prod
let inProd = () => {
  let processed = processFile("file.txt", ~ctx={"readfile": Node.Fs.readFileSync})

  processed
}

// Mock in test
let inTest = () => {
  let processed = processFile(
    "file.txt",
    ~ctx={
      "readfile": (_filename, _encoding) => "<mocked>",
    },
  )

  processed
}

You could use first class modules together with ReScript objects to get inferred DI as well:

module Fs = {
  external readFileSync: string => string = "readFileSync"
}

module type Fs = module type of Fs

module Other = {
  external log: string => unit = "console.log"
}

module type Other = module type of Other

let logContents = (contents, ~ctx) => {
  let log: string => unit = ctx["log"]
  log(contents)
}

let readAndReturnFile = (~ctx) => {
  let module(Fs: Fs) = ctx["fs"]
  let readFile: string => string = ctx["readFileSync"]
  switch readFile("test.txt") {
  | exception _ => Error(#ReadFileError)
  | fileContents => Ok(fileContents ++ " hello")
  }
}

let main = () => {
  let ctx = {
    "readFileSync": s => s,
    "log": Console.log,
    "fs": module(Other: Other),
  }

  switch readAndReturnFile(~ctx) {
  | Ok(contents) => contents->logContents(~ctx)
  | Error(#ReadFileError) => ()
  }
}
2 Likes