Skip to content

Instantly share code, notes, and snippets.

@foxnewsnetwork
Last active September 12, 2018 16:19
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save foxnewsnetwork/bd5169a483c390b5c05d7bfe0ff8103c to your computer and use it in GitHub Desktop.
ReasonML JavaScript API Bindings for Union Types

Problem Background

Recently, I've been trying to experiment with using ReasonML in production, however, this generally involves writing bindings to existing libraries... and unfortunately, some of these libraries expose extremely difficult to type interfaces; consider trying to wrap the following getUserInfo function (written in typescript for illustration purposes):

type xInfo = {
    info: string,
    message: string
};

type xError = {
    error: string,
    code: number
};

type xCallback = (response: xInfo | xError) => void;

declare function getUserInfo(cb: xCallback): void;

The difficulty in trying to bind this with bucklescript and ReasonML are the following:

  • There is only 1 callback and it must handle both success and failure
  • ReasonML doesn't support union types
  • It's a callback, not a promise

So what do?

Current Solution

After scouring the docs for:

I finally settled on using the "shady variant" method

First, I declare a "placeholder" type which exposes just enough for me to make a decision regarding response:

[@bs.deriving abstract]
type rawResp = {
  error: option(string)
};

(recall that deriving abstract takes us into record mode and creates helper methods for us - see the guide here)

I want to eventually coerce the rawResp type into a sensible "Either" type; perhaps something like:

type asyncResult =
  | Good(xInfo)
  | Fail(xError);

If rawResp contains Some(errorMsg), I will consider it an error, otherwise, I will consider it a success... so in code:

let castResponse = rawResp => switch(rawResp -> errorGet) {
| Some(_) => rawResp -> convertToError -> Fail
| None => rawResp -> convertToInfo -> Good
};

To build the two convertTo* functions, I use the "shady conversion" trick outlined here

external convertToError : rawResp -> xError = "%identity";
external convertToInfo : rawResp -> xInfo = "%identity";

Then, I can build a sensible Js.Promise.t based binding for the getUserInfo method:

exception AsyncError(xError);

[@bs.scope "sce"]
[@bs.val]
external _getUserInfo : rawResp => unit => unit = "getUserInfo";

type getUserInfo = unit => Js.Promise.t(xInfo);

let getUserInfo : getUserInfo = unit => 
  Js.Promise.make(
    (~resolve, ~reject) =>
      _getUserInfo(
        rawResp => switch(rawResp -> castResponse) { 
          | Good(xInfo) => resolve(. xInfo)
          | Fail(xError) => reject(. AsyncError(xError))
        }
      )
  );

Note: I had to use a exception AsyncError(xError) to wrap the exception; this is because the reject callback requires an exception object.

Note 2: the period in resolve(. xInfo) is required due to syntax constraints with uncurrying

Note 3: I have no idea if this is the correct method for wrapping such an API; but it works, so I'll just go with this for now. Perhaps one day, some ReasonML expert can give me some advice

References

@foxnewsnetwork
Copy link
Author

Update: apparently, according to @glennsl, we should avoid using a tagged union, see details here:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment