Sep 11, 2017

BuckleScript Gradual Typing: Incremental Change

IN THE previous post, I showed a basic example of overlaying an OCaml type over a dynamically-typed JavaScript function, using BuckleScript's FFI feature. In this post, I will show a slightly more complex example: binding to the Web API's Intl.DateTimeFormat constructor. As usual, you can follow along by typing and pasting code into the BuckleScript playground.

In JavaScript, we would use the constructor like:

const format =
  new Intl.DateTimeFormat("en-CA", { hour12: false });

We do need to note, though, that both of the arguments to Intl.DateTimeFormat are optional, and will be given default values if they are not passed in. So really in our BuckleScript binding we want to somehow make them optional as well.

Fortunately, OCaml and BuckleScript allow for optional function parameters, as long as we make the final parameter non-optional:

type dateTimeFormat

external dateTimeFormat_make :
  ?locales:'locales -> ?options:'options -> unit -> dateTimeFormat =
  "Intl.DateTimeFormat" []

let format =
    ~locales:"en-CA" ~options:[%obj { hour12 = false }] ()

We introduce several new concepts here:

type dateTimeFormat is an abstract type that, for now, is defined only by the fact that we create values of the type with:

external dateTimeFormat_make ... is a binding to a constructor (thanks to the [] attribute) which takes optional locales and options arguments, a required unit argument, and returns an instance of dateTimeFormat. The optional parameters are marked by the '?' symbol.

let format = ... is a usage of the dateTimeFormat_make FFI binding to force BuckleScript to actually generate a call to the Intl.DateTimeFormat constructor. By default BuckleScript only generates JavaScript output lazily.

In the spirit of gradual typing, I've also been lazy about working out the actual types of the parameters to the dateTimeFormat_make binding. Partly it's because the free type variables 'locales and 'options allow me to pass in any type of argument into those slots. In the previous snippet, I passed in the correct types (a string and a JavaScript object with a valid keyword) intentionally, but there is no type-level guarantee that I have to do so. I could have passed in values of invalid types, and BuckleScript would merrily generate JavaScript which would only fail at runtime.

So how can I force some more measure of type safety into the bindings here? Well, for the locales parameter, the Web API reference for Intl.DateTimeFormat says its type has to be either a string or an array of strings. In BuckleScript, we can model this 'either-or' scenario using the [@bs.unwrap] attribute:

external dateTimeFormat_make :
  ?locales:([ `one of string | `many of string array ] [@bs.unwrap]) ->
  ?options:'options ->
  unit ->
  dateTimeFormat =
  "Intl.DateTimeFormat" []

let formatOne = dateTimeFormat_make ~locales:(`one "en-CA") ()

let formatMany = dateTimeFormat_make ~locales:(`many [|"en-US"; "en-CA"|]) ()

Now, we can pass locales only as a single string or as an array of strings. Anything else is a type error. The compiler does need some help though, to statically tell the difference; hence we have to use polymorphic variants to differentiate one case from the other.

Note that I left out the options argument in these cases to make them simpler. If you paste this into the playground, you'll see BuckleScript generates undefined arguments in their place.

This post is getting rather long now, so I'll explore how to make the options argument more type-safe in the next one. It's going to be quite fun, because there's quite a lot we can do in BuckleScript to enforce the business rules of the API at the type level!

No comments: