How would you replicate this advanced dependency injection technique in rescript?

Take a look at this excellent article, section “Managing dependencies beyond partial application”:

I was trying to replicate the same technique in rescript:

module type ILogger = {
  let debug: string => unit
  let error: string => unit
}

module type ILog = {
  module Logger: ILogger
}

module Log = {
  let debug = (module(Env: ILog), str) => {
    Env.Logger.debug(str)
  }
  let error = (module(Env: ILog), str) => {
    Env.Logger.error(str)
  }
}

module type IDatabase = {
  let query: string => unit
  let execute: string => unit
}

module type IDb = {
  module Database: IDatabase
}

module Db = {
  let fetchUser = (module(Env: IDb), str) => {
    Env.Database.query(str)
  }
}

let execSomething =(env) =>  {
    let user = Db.fetchUser(env, "user patata")

    Log.debug (env, "User: what")

}

Unfortunately this doesn’t compile. How is this easily solved in the rescript / reasonml / ocaml world?

@not-rusty We can solve this by creating a module type that can be both IDb and ILog and using that in execSomething. The solution will look like this :point_down:

// Creating a type that can be both IDb and ILog
module type IDbLog = {
  include IDb
  include ILog
}

// Getting `IDbLog` as input 
let execSomething = (module(Env: IDbLog)) => {
  let _user = Db.fetchUser(module(Env), "user patata")
  Log.debug(module(Env), "User: what")
}

Playgroung link is available here.

Why not functors to inject module dependencies?

module type ILogger = {
  let debug: string => unit
  let error: string => unit
}

module type IDatabase = {
  let query: string => string
  let execute: string => unit
}

module MakeApp = (Db: IDatabase, Log: ILogger) => {
  module Query = {
    let fetchUser = str => {
      Db.query(str)
    }
  }

  module Log = {
    let debug = str => {
      Log.debug(str)
    }

    let error = str => {
      Log.error(str)
    }
  }

  let execSomething = () => {
    let user = Query.fetchUser("user patata")

    Log.debug("User: " ++ user)
  }
}

module MyDB: IDatabase = {
  let query = str => str
  let execute = str => ()
}

module MyLog: ILogger = {
  let debug = str => ()
  let error = str => ()
}

module App = MakeApp(MyDB, MyLog)

App.execSomething()

Playground Link

2 Likes

I wanted to make the compiler infer your IDbLog, as it does with the F# example, but I guess it’s not possible.

I think that’s the standard approach, but I was curious if there was a way to make the compiler infer the module dependencies, that’s why I used first class modules.

The nice thing in the F# article is that you can do something like this:

let foo env = // env :> IDb and env :> ILog
    let user = Db.fetchUser env 123	// env :> IDb
    Log.debug env "User: %A" user	// env :> ILog

And be properly inferred. I’m probably overcomplicating dependency injection, but that pattern in F# was nice. I probably could try something with open objects, but that’s overcomplicating the issue.

I think the more interesting question is what happens when you try to compile this code sample. I get the following error in the Playground:

[E] Line 37, column 14:
This has type: 
  Somewhere wanted: 

If you can replicate this at the command line, you should file a compiler bug report.

but I was curious if there was a way to make the compiler infer the module dependencies

Are you aware of the structural typing support in rescript, in that case, the compiler can infer it, but the error message sometimes look scary

Yeah, probably that’s the closest thing to fsharp interfaces. Whenever I have time I’ll try that out!

@not-rusty Hello :slight_smile: I ended up using the functor way proposed by @ryyppy.

You can check this article, i think that it could help you find the best solution for your needs :wink:

1 Like