Search code examples
javascriptocamlreasonbucklescriptunion-types

Untagged union from a javascript binding going down the wrong path


I am attempting to write a reasonml binding for the amqplib npm package:

http://www.squaremobius.net/amqp.node/

In particular this function:

http://www.squaremobius.net/amqp.node/channel_api.html#channel_get

class type amqpMessageT = [@bs] {
  pub content: nodeBuffer
};

type amqpMessage = Js.t(amqpMessageT);

type gottenMessage = Js.Nullable.t(amqpMessage);

type qualifiedMessage = Message(gottenMessage) | Boolean(bool);

class type amqpChannelT = [@bs] {
  pub assertQueue: string => queueParams => Js.Promise.t(unit);
  pub consume: string => (amqpMessage => unit) => unit;
  pub ack: amqpMessage => unit;
  pub get: string => Js.Promise.t(qualifiedMessage);
  pub purgeQueue: string => Js.Promise.t(unit);
  pub deleteQueue: string => Js.Promise.t(unit);
  pub sendToQueue: string => nodeBuffer => messageParams => unit;
};

And then I have the following code:

 ....
 channel##get("MyQueue")
 |> Js.Promise.then_(message => {
   switch message {
     | Boolean(false) => Js.Promise.resolve(Js.log("No Message"));
     | Message(msg) => Js.Promise.resolve(Js.log("Has Message, Will Travel"));
     | Boolean(true) => Js.Promise.resolve(Js.log("Impossible Message"!));
   }
  }

However this goes down the "Message(msg)" path always, even when the js call returns false.

Now adding the following binding:

let unsafeGet: amqpChannel => string => Js.Promise.t(gottenMessage) = [%bs.raw{|function(channel, queueName) {
  return channel.get(queueName).then((value) => {
    if(value === false) {
      return Promise.resolve(null)
    } else {
      return Promise.resolve(value)
    }
  })
}|}];

I've been able to sidestep the problem, but I'm not a huge fan of using bs.raw if I'm honest. What's the issue with my initial untagged union type? How can I fix this problem?


Solution

  • There is no untagged union type, and no run time type information in the OCaml language, so you'll have to implement your own type, checks and conversions to get it into a usable form.

    You could for example use an abstract type to represent the "unknown" type, and a companion function to check its type, cast it to that type, and then convert it to a qualifiedMessage:

    type unknownMessage;
    
    let classifyMessage = (value: unknownMessage) =>
      switch (Js.Types.classify(value)) {
      | JSString(s) => Message(Js.Nullable.return(s))
      | JSNull      => Message(Js.null)
      | JSFalse     => Boolean(false)
      | JSTrue      => Boolean(true)
      | _           => failwith("invalid runtime type")
      }
    

    Also, as a sidenote, if you abstract away the underlying data structure by exposing abstract types and functions/externals instead of exposing the "raw" objects you'll gain a lot of flexibility in how you define the interface and can hide this extra conversion step.