trite.io - posts

Basics of relude-fetch library in ReasonML

Created: March 5, 2023

Why?

Finally creating a site to house the content that lives in my trite.io-content repository. The end goal is to build a javascript app via ReasonML that will fetch its actual content through Github API calls to the trite.io-content repo.

One important step for this means using the fetch api. This can mean creating bindings by hand from Reason or using the bs-fetch library. Fortunately I like working with the Relude standard library already, and there’s an accompanying library relude-fetch which makes it easy to work with fetch in a more idiomatic functional programming way!

The problem

To use fetch from ReasonML means either creating bindings by hand or using a library that has taken care of this already.

The bs-fetch library knocks out the basics, but leaves us with promises as the main method of interaction:

Js.Promise.(
  Fetch.fetch("/api/hellos/1")
  |> then_(Fetch.Response.text)
  |> then_(text => print_endline(text) |> resolve)
);

Promises are fine and all, but both JavaScript and ReasonML have better mechanisms for dealing with asynchronous behavior.

In JavaScript this usually means using async/await. In functional languages like ReasonML this often means using monads, which is what this post will focus on.

The solution

relude-fetch takes the bs-fetch promise and converts it to use Relude.IO. This takes something that composes poorly and turns it into a compositional powerhouse!

Here is where relude-fetch takes the bs-fetch bindings and turns them up to 11:

let fetch: string => Relude.IO.t(Fetch.response, Js.Promise.error) =
  url =>
    Relude.IO.suspendIO(() => Relude.Js.Promise.toIO(Fetch.fetch(url)));

So how exactly does one use this?

Usage

Start by defining the type interface for errors. Doing this first will simplify later usage:

module Error = {
  type t = ReludeFetch.Error.t(string);
  let show = error => ReludeFetch.Error.show(a => a, error);
  module Type = {
    type nonrec t = t;
  };
};

module IOE = IO.WithError(Error.Type);

Once done you can then access the available infix operators to further simplify usage:

open IOE.Infix;

Now you have access to the monadic bind operator >>= which allows for easier function composition. For example one could write a simple function to take a uri as input and return either the page content as a string, or else an error:

let fetchString = uri => ReludeFetch.fetch(uri) >>= ReludeFetch.Response.text;

Here’s what it looks like to use our newly created fetchString command:

uri
|> fetchString
|> IO.map(content => setter(_ => content))
|> IO.mapError(error => setter(_ => error |> ContentFetch.Error.show))
|> IO.unsafeRunAsync(Result.fold(() => (), () => ()));

Here is what’s going on in the above code:

More reading