Welcome! We notice you're using an outdated browser, which will result in a degraded experience on this site. Please consider a modern, fully supported browser.

webbureaucrat

The articles are just window-dressing for code snippets I want to keep.

Dynamic Options and Optional Parameters in ReasonML

The 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) or foo(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 lists.

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.

← Home