Reflexion about some good practice concerning Functors

Hello all, i need some afterthoughts about my way of doing things.
Considering a repository named ReadModel in the file Repositories.res:

module ReadModel = {
  type find = Read.id => AsyncResult.t<option<Read.t>, string>
  type save = Read.t => AsyncResult.t<unit, string>
}

and an in-memory implementation in the file InMemory.res

module ReadModel = () => {
  open Read
  let storage = ref(Ok(Belt_MapString.empty))
  let errorStorage = (error: string) => storage := Error(error)
  let fillStorage = models =>
    storage :=
      Ok(Belt_MapString.fromArray(models->Js_array2.map(model => (model.id->toString, model))))

  let find: Repositories.ReadModel.find = modelId =>
    storage.contents->AsyncResult.mapFromResult(beltMap =>
      beltMap->Belt_MapString.get(modelId->toString)
    )

  let save: Repositories.ReadModel.save = model =>
    storage.contents->AsyncResult.mapFromResult(beltMap =>
      storage := Ok(beltMap->Belt_MapString.set(model.id->toString, model))
    )
}

I use this technic in order to have an “execution context” during my unit test.
My question is this: It is a good practice to use Functor to isolate execution ? Is there a better way ?
I fairly new to Functional Programming, and i wonder if you guys have some advice about this subject.

Best regards :wink:

This is my personal opinion, so other people may think differently.

Functors are easy to use in ReScript, but they’re also pretty heavy and are usually not needed. In your ReadModel functor, it looks like its purpose is to create a storage value. It could be easier to just create the value with a function and then pass that value as a parameter to the other functions.

Example:

let makeStorage = () => ref(Ok(Belt.Map.String.empty))
let errorStorage = (storage, error) => storage := Error(error)
// usage:
let storage = makeStorage()
errorStorage(storage, "something broke")

In the functor version, you end up creating all of the other values as well, not just the storage. This makes the code harder for the compiler to optimize, since the functions have to be created dynamically at runtime instead of statically at compile time. (It’s also less tree-shakable in JavaScript.)

Using the “principle of least power,” it’s good to try to use the simplest tool to solve each problem. One main advantage to functors is that they can create types. If you aren’t defining any types in your functor, then that’s a sign that the functor probably isn’t needed, and you should use plain-old functions instead. When you do need a functor, it’s usually best to keep it as “small” as possible by only defining a minimum number of types and values inside it.

2 Likes

as @johnj explained, functions are indeed usually enough for “dependency injection”.

On a different topic, try to use the proper name for a module instead of the internal one (Belt.Map.String, Js.Array2 vs Belt_MapString, Js_array2, etc). The internal names are considered implementation details and are subject to changes. I would also generally recommend you to use Belt functions instead of Js (for array map here), it does add a slight weight, but they’re usually more performant.

3 Likes