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" [@@bs.new]
let format =
dateTimeFormat_make
~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 [@@bs.new]
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" [@@bs.new]
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!