Record of functions & generic types

Hello,

I’m exploring how rescript can be used along with typescript (in order to ease adoption at work) and I can’t figure out how to solve this problem :

  • define a logger that has several “methods” (like info, error etc) they use a generic type so it’s something like info: ('a, string) => 'a (like the Debug.Trace in Haskell)
  • I have some business logic that takes this logger type as parameter so I don’t have to explicitly pass every method (like an interface in OO, a record of functions in Haskell)

The overall idea is to have an hexagonal architecture, the whole domain modelling part (“usecase + domain”) would be in rescript while the adapters & main would be in typescript.

I have 2 failed attempts but for 2 different reasons :slight_smile:

version 1 : using a functor, I can do something like :

module type Logger1 = {
  let info: ('a, string) => 'a
  let error: ('a, string) => 'a
}

module BusinessLogic1 = (Logger: Logger1) => {
  @genType
  let do = (age: int): unit => {
    age->Logger.info("hello")->ignore
    "aString"->Logger.info("hello!")->ignore
    ()
  }
}

problem : it looks nice on the rescript side… but genType generates nothing in ts


version 2 : I try to pass everyting as parameter

module Logger2 = {
  type info<'a> = ('a, string) => 'a
  type error<'a> = ('a, string) => 'a

  // type info = ('a, string) => 'a <- this doesn't work because 'a is unbound...
}

module BusinessLogic2 = {
  @genType
  let do = (age: int, logInfo: Logger2.info<'a>): unit => {
    age->logInfo("hello")->ignore
    //"aString"->logInfo("hello")->ignore // this doesn't work because 'a has been inferred as int
    ()
  }
}

problem : it doesn’t really solve the problem on the rescript side… but it generates ts code

1 Like

ok, I’ve got something working. It doesn’t solve the rescript-ts problem above, but solves my concrete one.

Instead of trying to use lots of genTypes in order to use it from ts I use the js-interop decorators to use a local ts module (with a bit of trial & error, I really should dig deeper on this subject).

  • domain → rescript + a bit of genType
  • usecase → rescript + genType only for a few function types
  • adapters → local ts module <-> external lib + local rescript js interop file
  • main → rescript

here’s the test repository : https://github.com/err0r500/realworld-app-backend-rescript-ts

1 Like

This may not help with your larger problems, but if you were interested in knowing how to get your version 2 working (the one where you pass in the logging function directly), here is how you could do it.

The problem is that you want to use a polymorphic function as an argument to a higher-order function. You need to explicitly annotate the input type of the first argument of the log function as polymorphic, but it is tricky because you can’t do it directly.

(Note: The PinoLogger module is simplified version from the one in the git repository you linked.)

// Simplified from src/driven/loggerPino.res
// Nothing is changed here except the name and no local open for clarity.
module PinoLogger = {
  let pino = Pino.instanciate()

  let log = (value: 'a, msg: string, f: (Pino.t, string) => unit): 'a => {
    let concat = (a: 'a, m: string) =>
      Js.Json.stringifyAny(a)->Belt.Option.mapWithDefault(
        m ++ ": non serializable value provided",
        v => m ++ ": " ++ v,
      )

    pino->f(concat(value, msg))->ignore
    value
  }

  let info = (a: 'a, s: string): 'a => log(a, s, Pino.logInfo)
}

module Logger = {
  // You need to annotate 'a as polymorphic, but you can't do it directly here.
  type info<'a> = ('a, string) => 'a

  // Instead, you have to use a universally quantified record field to do it.
  // Here, 'a is explicitly annotated as being polymorphic.
  type info' = {info: 'a. info<'a>}

  // Helper so you don't have to mess with records as much.
  let info = {info: LoggerPino.info}
}

module BusinessLogic = {
  // Write {info} instead of info so that you can call it directly without
  // writing info.info(...).  
  let do = (age: int, {info}: Logger.info'): unit => {
    // This works.
    age->info("hello")->ignore
    // And so does this one!
    "aString"->info("hello")->ignore
  }
}

// Test it out!
BusinessLogic.do(35, Logger.info)

// Running on Node gives you something like this
// {
//   level: ...,
//   time: ...,
//   pid: ...,
//   hostname: "...",
//   msg: "hello: 35",
// }
//
// {
//   level: ...,
//   time: ...,
//   pid: ...,
//   hostname: "...",
//   msg: 'hello: "aString"',
// }

(I know the contributing guide says not to link to OCaml docs, but I can’t find any explanations of this in the ReScript docs…so if you want to find out more about it, check out the OCaml manual.)

2 Likes

great ! thanks a lot ! I don’t think I’ll use it in this case, but I’m pretty sure it will be useful somewhere else ! … and I learnt something, that’s always cool ! :slight_smile:
thanks @Ryan

1 Like