Modeling a Polymorphic High-Order Callback Parameter


#1

I’m working on some bindings for a library that uses a higher-order, polymorphic callback for signifying done-ness of async actions. I’m struggling with how to model/deal with this in Reason/Bucklescript.

The function is used like so:

_(push => {
  Promise.all([
      axios.get(url1),
      axios.get(url1),
  ])
      .then(([ res1, res2 ]) => {
          push(null, res1) // push a value
          push(null, res2) // push a value
      })
      .catch(err => push(err, null)) // push an error
      .finally(_ => push(null, _.nil)) // say "we're done here"
})

As you can see, push has 3 potential values for its second argument: null, some 'a, and _.nil.

I’ve modeled the null and 'a usages like below, but adding in the additional _.nil variant has me stumped. I looked at [@bs.unwrap], but this doesn’t strike me as being useful for defining the type of push.

type t('e, 'a);
type push('e, 'a) = (~error: 'e=?, ~value: 'a=?, unit) => unit;
[@bs.module] external generator: (push('e, 'a) => unit) => t('e, 'a) = "_";

Is there some idiomatic way to model this function?


#2

I’d bind that with the simplest possible type and then wrap it with a more type-safe wrapper that enforces that you’re only pushing a value or an error, and never both.

[@bs.module "whatever-module"] external push: (Js.Null.t(Js.Promise.error), Js.Null.t('a)) => unit = "";

let push = fun
  | Js.Result.Ok(value) => push(Js.Null.empty, Js.Null.return(value))
  | Error(err) => push(Js.Null.return(err), Js.Null.empty);

// Examples

push(Js.Result.Ok("hello"));
push(Js.Result.Error(err));

Now it doesn’t matter what type _.nil is, the push function is polymorphic in the ‘value’ type.


#3

@yawaramin, that makes sense, but I think that gets back to my point about why I didn’t use [@bs.unwrap].

This isn’t a function that is made available on the library level; this function is passed as the first argument to the callback I provide:

const myCallback = (PUSH, _next) => { /* my async logic */ }
someOtherFunction(myCallback)

#4

You can follow the same strategy even for a higher-order callback like this. The key is to write down an accurate type for someOtherFunction and then wrap it to take a safer push callback:

[@bs.module "some-module"] external someOtherFunction: (
  (. (Js.Null.t(Js.Promise.error), Js.Null.t('a)) => unit) => unit,
) => unit = "";

let someOtherFunction(callback) = someOtherFunction((. push) =>
  callback(fun
    | Js.Result.Ok(value) => push(Js.Null.empty, Js.Null.return(value))
    | Error(err) => push(Js.Null.return(err), Js.Null.empty)));

Now you can call your wrapped version of someOtherFunction with a safe push callback:

someOtherFunction(push => {
  push(Js.Result.Ok("hello"));
  push(Js.Result.Error(err));
});

#5

I might be being a little bit thick here, @yawaramin, but that looks like what I’ve already got, but with the addition of Js.Result.

What you have above as the possible types for push's second argument are null and 'a (aka Js.Null.t('a)), but what I’m struggling with is a third possible value, which is one provided by the library:

type nil;
[@bs.module "some-module"] [@bs.val] external nil: nil = "";
someOtherFunction((. push) => {
    push(Js.Result.Ok("hello"));
    push(Js.Result.Error(err));
    push(nil);
});

Is there a way I can extrapolate your example above to also meet this requirement?


#6

Try it as is and see if you get any errors :slight_smile: I believe since the callback is polymorphic in the value type, it should just work.


#7

I took that example you posted above and added the nil definition and usage like so:

type nil;
[@bs.module "some-module"] [@bs.val] external nil: nil = "";
[@bs.module "some-module"]
external someOtherFunction:
  ((. ((Js.Null.t(Js.Promise.error), Js.Null.t('a)) => unit)) => unit) =>
  unit =
  "";

let someOtherFunction = callback =>
  someOtherFunction((. push) =>
    callback(
      fun
      | Js.Result.Ok(value) => push(Js.Null.empty, Js.Null.return(value))
      | Error(err) => push(Js.Null.return(err), Js.Null.empty),
    )
  );

someOtherFunction(push => {
  push(Js.Result.Ok("hello"));
  push(nil);
});

This does not work in the way that I expected:

>>>> Start compiling
[2/2] Building src/Example.mlast.d
[1/1] Building src/Example-foo.cmj

  We've found a bug for you!
  /foo/src/Example.re 20:8-10

  18 │ someOtherFunction(push => {
  19 │   push(Js.Result.Ok("hello"));
  20 │   push(nil);
  21 │ });

  This has type:
    nil
  But somewhere wanted:
    Js.Result.t(string, Js.Promise.error) (defined as
      Js.Result.t(string, Js.Promise.error))

>>>> Finish compiling(exit: 1)

#8

OK, gotcha. So we want to push either of three things:

  1. An error as the first argument
  2. A data value as the second argument
  3. A ‘we’re finished’ value as the second argument

The first two are easy because of the Js.Result.t type, but for the third one we’ll need to adjust the way the binding works. I recommend creating an abstract data type of which nil is a member, but which also allows casting arbitrary values:

type data;
[@bs.module "some-module"] external nil: data = "";
external data: _ => data = "%identity";

[@bs.module "some-module"] external someOtherFunction: (
  (. (Js.Null.t(Js.Promise.error), Js.Null.t(data)) => unit) => unit,
) => unit = "";

let someOtherFunction(callback) = someOtherFunction((. push) =>
  callback(fun
    | Js.Result.Ok(value) => push(Js.Null.empty, Js.Null.return(value))
    | Error(err) => push(Js.Null.return(err), Js.Null.empty)));

Now you can convince the compiler that you’re always calling push with the correct type, Js.Result.t(data, Js.Promise.error):

someOtherFunction(push => {
  open Js.Result;

  push(Ok(data("hello")));
  push(Ok(nil));
});

The crucial thing here is the extra function call data which tells the compiler that the actual data and the nil have the same type. This extra call is optimized away because it’s defined as an external.


#9

That makes perfect sense. Thanks so much for taking the time to understand the problem and help me understand how to use the type system a little better!