Dynamic Options and Optional Parameters in ReasonML
bucklescript monad reasonmlThe next type I want to bind from the
JavaScript ServiceWorker API
is Cache. At first
glance, Cache
doesn't have any dependencies on any JavaScript interfaces we don't already
have access to, but its methods do use dynamic JavaScript options
parameters,
and the way we deal with this in typed languages is to name and create new
specialized types. In this post, I will implement types for these options
and the functions that use them.
A word on the many definitions of options.
Unfortunately, this post requires using three different meanings for option all in the same context. I will distinguish between them as follows:
option
monad - is a type built into ReasonML which helps to handle cases when a value may or may not exist. Monads are important in functional programming, and it's not always easy for newcomers, so if you're unaccustomed, I suggest studying up.- optional parameters - Parameters that a function doesn't necessarily need
in order to be called (e. g., a function that can be invoked as either
foo(y)
orfoo(x, y)
. In ReasonML, optional parameters often use option monads, but they don't have to. For further reading, see the docs. options
dynamic object (associative array) - a common name for an optional parameter for many JavaScript functions. In our binding, it will correspond directly to a statically-typed ReasonML record.
For this reason I'm going to avoid referring to "options" by itself and stick to the phrases I've defined above. If you're still confused, feel free to @ me.
Defining the type as a record.
Looking at the documentation for
Cache, I can see
that the first method on the list,
match takes a
Request
and a dynamic options
object. Request
is already available in
this project through bs-fetch, so we
will here focus on the options
dynamic object.
Defining the type itself is almost trivial; all we need to do are to add the
expected property names and type names from match()
's documentation and copy
those into a ReasonML record
like so:
type cacheMatchOptions =
{
ignoreSearch: bool,
ignoreMethod: bool,
ignoreVary: bool
}
Now, let's start to implement Cache. We'll start by opening bs-fetch for the response and request types and declaring our new type.
open Fetch;
type cache;
Next, let's get our type signature going.
let match = (cache, ~options=?, ~req: Request.t): Js.Promise.t(Response.t) =>
{
/* TODO */
}
There's a lot going on here. From left to right, we have a name match
, an
unnamed parameter of type cache
, a named, optional parameter options
, a
Request
typed parameter named req
, and a return type of a promise of a
response.
I want to spend some time on that middle parameter called options
. The "~"
makes it a named parameter, as opposed to cache
which is identified by it's
type. The =
after the name makes it optional---we don't need it in order
to call match
. The thing after the equals sign helps determine its type. If
we had followed it with a value of some kind, then that value would be used
as the default value whenever we called match
without that parameter, but
instead we followed it with a question mark, meaning that the parameter will
be typed as an option
monad, defaulting to None
when there is no value
passed in.
As a side note, the type of the options
parameter can't fully be
determined right now.
I didn't add one explicitly because that function signature is already pretty
long. The compiler will be able to infer the type after we've implemented the
rest of the function. Let's get to it.
let match = (cache, ~options=?, ~req: Request.t): Js.Promise.t(Response.t) =>
{
switch(options) {
| None => /* TODO */
| Some(o) => /* TODO */
}
};
The options
parameter is an option
monad, so we need to unwrap it with a
switch before we can use the underlying value, which we've just named o
.
We're now set up to handle both cases: one where the function is not passed
an options
parameter, and one where it is passed. Now we need to write
two bindings: one for each case, and call those bindings inside our switch. I
don't want the library consumers to call these bindings directly, though, so
I'm going to wrap them in a module called Private
as a signal to users.
module Private = {
[@bs.send] external matchWithoutOptions: (cache, Request.t)
=> Js.Promise.t(Response.t) = "match";
[@bs.send] external matchWithOptions:
(cache, Request.t, CacheMatchOptions.t)
=> Js.Promise.t(Response.t) = "match";
};
let match = (cache, ~options=?, ~req: Request.t): Js.Promise.t(Response.t) =>
{
switch(options) {
| None => Private.matchWithoutOptions(cache, req)
| Some(o) => Private.matchWithOptions(cache, req, o)
}
};
This will compile and correctly infer the types. It now understands that the
options
parameter is an option
monad of a
CacheMatchOptions.t
through the wild magical
inferencing that comes with ReasonML.
Let's do that all again to bind the matchAll
function. It's the same thing
over again but with list
s.
module Private = {
[@bs.send] external matchWithoutOptions: (cache, Request.t)
=> Js.Promise.t(Response.t) = "match";
[@bs.send] external matchWithOptions: (cache, Request.t, CacheMatchOptions.t)
=> Js.Promise.t(Response.t)
= "match";
[@bs.send] external matchAllWithoutOptions: (cache, Request.t)
=> Js.Promise.t(list(Response.t)) = "match";
[@bs.send] external matchAllWithOptions:
(cache, Request.t, CacheMatchOptions.t)
=> Js.Promise.t(list(Response.t)) = "match";
};
let match = (cache, ~options=?, ~req: Request.t): Js.Promise.t(Response.t) =>
{
switch(options) {
| None => Private.matchWithoutOptions(cache, req)
| Some(o) => Private.matchWithOptions(cache, req, o)
}
};
let matchAll = (cache, ~options=?, ~req: Request.t):
Js.Promise.t(list(Response.t)) =>
{
switch(options) {
| None => Private.matchAllWithoutOptions(cache, req)
| Some(o) => Private.matchAllWithOptions(cache, req, o)
}
};
Continuing through MDN's list of Cache
methods, let's add bindings for a few
more functions.
[@bs.send] external add: (cache, Request.t) => Js.Promise.T(unit)
= "add";
[@bs.send] external addAll: (cache, list(Request.t)) => Js.Promise.T(unit)
= "addAll";
[@bs.send] external put: (cache, Request.t, Response.t) => Js.Promise.T(unit)
= "put";
Lastly, we have two more functions with options
dynamic objects as
parameters. Each of these takes an options
similar to the CacheMatchOptions
we defined in the beginning, but with an additional property, cacheName
.
I used the same options
record type to represent the options
dynamic
objects in match
and matchAll
because that's fairly intuitive. I wouldn't
expect those definitions to change independently. However, I'm pretty reluctant
to do the same for the
delete and
keys functions.
Besides, since if I had one record type to serve both purposes, I don't think
I could name it cleanl and intuitively. It's better to make two record types.
src/CacheDeleteOptions.re
type cacheDeleteOptions =
{
ignoreSearch: bool,
ignoreMethod: bool,
ignoreVary: bool,
cacheName: string
}
type t = cacheDeleteOptions;
src/CacheKeysOptions.re
type cacheKeysOptions =
{
ignoreSearch: bool,
ignoreMethod: bool,
ignoreVary: bool,
cacheName: string
}
type t = cacheKeysOptions;
Then we take what we did for match
and matchAll
and apply it to delete
and keys
, respectively. I'm also going to break Private
into more modules
because I like how the short function names look.
open Fetch;
type cache;
module Private = {
module Delete = {
[@bs.send]
external withOptions:
(cache, Request.t, CacheDeleteOptions.t) => Js.Promise.t(bool) =
"delete";
[@bs.send]
external withoutOptions: (cache, Request.t) => Js.Promise.t(bool) =
"delete";
};
module Keys = {
module WithRequest = {
[@bs.send]
external withoutOptions:
(cache, Request.t) => Js.Promise.t(list(Request.t)) =
"keys";
[@bs.send]
external withOptions:
(cache, Request.t, CacheMatchOptions.t) =>
Js.Promise.t(list(Request.t)) =
"keys";
};
module WithoutRequest = {
[@bs.send]
external withoutOptions: cache => Js.Promise.t(list(Request.t)) =
"keys";
[@bs.send]
external withOptions:
(cache, CacheMatchOptions.t) => Js.Promise.t(list(Request.t)) =
"keys";
};
};
module Match = {
[@bs.send]
external withoutOptions: (cache, Request.t) => Js.Promise.t(Response.t) =
"match";
[@bs.send]
external withOptions:
(cache, Request.t, CacheMatchOptions.t) => Js.Promise.t(Response.t) =
"match";
};
module MatchAll = {
module WithRequest = {
[@bs.send]
external withoutOptions:
(cache, Request.t) => Js.Promise.t(list(Response.t)) =
"match";
[@bs.send]
external withOptions:
(cache, Request.t, CacheMatchOptions.t) =>
Js.Promise.t(list(Response.t)) =
"match";
};
module WithoutRequest = {
[@bs.send]
external withoutOptions: cache => Js.Promise.t(list(Response.t)) =
"match";
[@bs.send]
external withOptions:
(cache, CacheMatchOptions.t) => Js.Promise.t(list(Response.t)) =
"match";
};
};
};
let match = (cache, ~options=?, ~req: Request.t): Js.Promise.t(Response.t) => {
switch (options) {
| None => Private.Match.withoutOptions(cache, req)
| Some(o) => Private.Match.withOptions(cache, req, o)
};
};
let matchAll = (~options=?, ~req=?, cache): Js.Promise.t(list(Response.t)) => {
switch (req) {
| None =>
switch (options) {
| None => Private.MatchAll.WithoutRequest.withoutOptions(cache)
| Some(o) => Private.MatchAll.WithoutRequest.withOptions(cache, o)
}
| Some(r) =>
switch (options) {
| None => Private.MatchAll.WithRequest.withoutOptions(cache, r)
| Some(o) => Private.MatchAll.WithRequest.withOptions(cache, r, o)
}
};
};
[@bs.send] external add: (cache, Request.t) => Js.Promise.t(unit) = "add";
[@bs.send]
external addAll: (cache, list(Request.t)) => Js.Promise.t(unit) = "addAll";
[@bs.send]
external put: (cache, Request.t, Response.t) => Js.Promise.t(unit) = "put";
let delete = (cache, ~options=?, ~req: Request.t): Js.Promise.t(bool) => {
switch (options) {
| None => Private.Delete.withoutOptions(cache, req)
| Some(o) => Private.Delete.withOptions(cache, req, o)
};
};
let keys = (~options=?, ~req=?, cache): Js.Promise.t(list(Request.t)) => {
switch (req) {
| None =>
switch (options) {
| None => Private.Keys.WithoutRequest.withoutOptions(cache)
| Some(o) => Private.Keys.WithoutRequest.withOptions(cache, o)
}
| Some(r) =>
switch (options) {
| None => Private.Keys.WithRequest.withoutOptions(cache, r)
| Some(o) => Private.Keys.WithRequest.withOptions(cache, r, o)
}
};
};
And that's our type. I'm not sure, but I think I'm pretty close to having enough types implemented to write a small ServiceWorker. Hopefully I'll be able to write that post in the near future.
I write to learn, so I welcome your constructive criticism. Report issues on GitLab.