Modeling Polymorphic Callback

Hi folks,
I’m trying to finish my simple Nodejs backend with Rescript, and so far the whole process is successful expect one element.
I use Passport local authentication. It requires to define callback function:

(username, password, done) => ()

where done can be:

// success
done(null, user)

or

// failure
done(null, false)

or

//error
done(err)

It wouldn’t be a problem for polymorphic functions, but unfortunately, I have callbacks here. I thought I will be able to handle it with @unwrap but it doesn’t work.

I typed done as:

type passType<'a> =
  @unwrap
  [
    | #Bool(bool)
    | #Str(string)
  ]

type done<'a, 'b> = (
  .
  Js.Nullable.t<'b>,
  passType<'a>
) => unit

and later used it like this:

let strategy = Passport.makeLocalStrategy((. username, password, done) => {
  UserDao.Get.verifyUser(~name=username, ~password)
  ->thenResolve(user => {
    switch user {
    | Ok(user) => done(. Js.Nullable.null, #Str(user.id))
    | Error(_) => {
        Js.log("error")
        done(. Js.Nullable.null, #Bool(false))
      }
    }
  })
  ->ignore
})

but unfortunately, @unwrap doesn’t work here and the output code for #Bool looks like this:

              return done(null, {
                          NAME: "Bool",
                          VAL: false
                        });

To be honest I have no idea how to handle it. I could write the whole module as %%raw but it would look pretty bad. Is it even possible with Rescript?

Wow, that’s a convoluted callback. It should be possible though. You are on the right track, just remember that it should mostly be defined using an external:

external makeLocalStrategy: (
  string,
  string,
  (. Js.Nullable.t<exn>, option<PassType.t>) => unit,
) => ...

The key to this binding is the PassType.t type, which needs to be defined in such a way that you can create directly false or any other value with it. For this, you need a little bit of ‘raw’ or ‘magic’, whichever you prefer:

module PassType: {
  type t

  let false: t
  let userId: string => t
} = {
  type t

  let false = Obj.magic(false)
  let userId = Obj.magic
}

We are using an unsafe casting, but it is wrapped up in the implementation and not exposed by the abstract type. So we are tightly controlling the unsafety. Abstract types are in general super important in ReScript for safely modelling many dynamic patterns in JS.

3 Likes

That works like a charm! And I really like how unsafe code is wrapped in implementation.
This kind of APIs from external libraries are rare but happens sometimes, especially in the Express.js world that is full of variadic functions.
I love Rescript on frontend and I wanted to try how it would work on backend (as I struggled with typescript so many times). I found it awesome converting js into functional code (I love @send) and actually easy to write. I will probably share my example as I managed to use the whole stack: express + express-session + passport + knex (with postgresql) and learned so much!
It’s so great that Rescript gives the ability to model even such a case!
I still need to process a bit the syntax, as describing a type for a module looks new to me (I can’t find it in the documentation under modules and Converters.

It’s mentioned in the section Signatures, but it’s not explained that a signature can be inline like I showed. It’s an important technique imho. I use it a lot. It should probably be added to the docs there.

2 Likes

To be honest, as I am a newcomer to Ocaml world and stuff like functors I overlooked it, mistakenly thinking it was about functors. It was my inattention, but maybe adding it to docs could help others like me.