Functor Bindings for Polymorphic Functions

It can be very tedious writing Rescript bindings for Javascript functions that have one or more parameters that can have two or more different types. In rescript a type can’t be the union of a string and a boolean which is different from e.g. typescript. So to deal with polymorphic parameters, one solution is to write a completely new binding for each case, which can lead to very repetitive code and reduced readability (I’ll show an example of this later).

This article will show examples on how we can improve this repetitve code by using functors a.k.a. module functions. If you want, you can jump straight to the last chapter where I show a real world example using a functor… but I think it’s quite an advanced example so that’s why the preface is a bit long. First I’ll explain the problem in more detail, then show a made up example using a functor and then finally a more real-world example

Problem

Let’s look at the problem more closely

function printTypeAndValue(value) {
  console.log(`Type: ${typeof value}, Value: ${value}`);
}

// Example usage
printTypeAndValue(42);
printTypeAndValue("Hello");
printTypeAndValue([1, 2, 3]);
printTypeAndValue({ name: "Alice" });

and the binding could look like this

external printTypeAndValue: 'a => unit = "printTypeAndValue";

but this function now takes in whatever type and that’s not type safe. That would also allow us to call the function with types that don’t have an equivalent in Javascript, like tuples or polyvariants and that would maybe lead to unexpected output.

Lets say we don’t want to be able to pass an object nor an array but only int and string. So we’d maybe want to write out


external printTypeAndValueInt: int => unit = "printTypeAndValue";
external printTypeAndValueString: string => unit = "printTypeAndValue";
//.... and etc. We have to add a new binding for every type

/* Example usage */
let () = {
  printTypeAndValueInt(42);
  printTypeAndValueString("cat")
};

And you can see we’d have to add a new binding for every single value because in Rescript we can’t say that a value can be string or boolean, it’s just either one. Imagine if there would be even more parameters both polymorphic and monomorphic - there would be lots of repetitions. There are other ways to make these kind of bindings in a more clever way but using module functors is very practical to make code more readable if you have many polymorphic parameters as we’ll see later in this article

Made up recipe example using a functor

Let’s take a little more realistic example with two polymorphic parameters. We have this recipe component that we want to bind to.

  • First parameter is amount and if it’s false it means that the recipe does not indicate number of portions (servings) the recipe yields.
  • The second parameter is the name of the recipe, fx. “Pita”, but sometimes there are array of strings if the recipe consists of several recipes: “Pita bread”, “Pita dressing” and “Pita stuffing”

Maybe it’s not the best API that exists and could probably be improved but lets just stick to this example

Here we could have 4 different cases:

  • portions: boolean, items: string
  • portions: boolean, items : array
  • portions: int, items: string
  • portions: int, items: array

instead of having to do something like

module RecipePolymorphic = {
  @module("@Recipe") @react.component
  external make: (~portions: 'a, ~items: 'a,  description: string, ingredients:array:<string> ) => React.element = "default"
}

which like we saw earlier is not very type safe… you can make a binding for each case:


module NoPortionRecipe = {
  @module("@Recipe") @react.component
   external make: (~portions: bool, ~items: string, description: string, ingredients:array:<string> ) => React.element = "default"
}


module NoPortionRecipeWithSideDish  = {
  @module("@Recipe") @react.component
   external make: (~portions: bool, ~items: string, description: string, ingredients:array:<string> ) => React.element = "default"
}

module RecipeWithSideDish = {
  @module("@Recipe") @react.component
   external make: (~portions: int, ~items: array<string>, description: string, ingredients:array:<string> ) => React.element = "default"
}

module Recipe = {
  @module("@Recipe") @react.component
   external make: (~portions: int, ~items: string, description: string, ingredients:array:<string> ) => React.element = "default"
}

what is so tedious about this is that you’d have to repeat this pattern over and over again, both this part @module("@Recipe") @react.component and you even have to repeat all the other parameters that are not polymorphic. Imagine if we’d have plenty of other parameters, it would be even more repetitive

Using module functors you can instead pass the types into the binding, like passing parameter in functions

module MakeRecipe = (
  T: {
    type p
    type i
  },
) => {
  type p = T.p
  type i = T.i
  @module("@Recipe") @react.component
  external make: (~portions: p, ~items: i, description: string, ingredients:array:<string> ) => React.element = "default"
}

module NoPortionRecipe = MakeRecipe({
  type p = bool
  type i = string
})

module NoPortionRecipeWithSideDish  = MakeRecipe({
  type p = bool
  type i = array<string>
})

module RecipeWithSideDish = MakeRecipe({
  type p = int
  type i = array<string>
})

module Recipe = MakeRecipe({
  type p = int
  type i = string
})

A real world example using functor

The first time I saw this pattern was at Carla where I’m working. For the bindings to our design system we’re using a module functor.

There we have a component called <Box> that can take in many different kinds of values

  • <Box width=100> or
  • <Box width={[”100%”, 20, 30, 40]} /> or
  • <Box direction=["row", "column"]/>

so it can take in either string, or int or an array of either strings or ints - very polymorphic. So what we do to handle this is something like this:

module MakeResponsive = (
  T: {
    type t
  },
) => {
  type t = T.t
  external skip: t = "#undefined"
  external responsive: array<t> => t = "%identity"
}

module ResponsiveInt = MakeResponsive({
  type t = int
})
module ResponsiveString = MakeResponsive({
  type t = string
})

module ResponsiveIntAndString = {
  include ResponsiveInt
  external string: string => t = "%identity"
}
module Box = {
  module Width = ResponsiveIntAndString
  module Flex = ResponsiveInt
  module Display = ResponsiveString
  module Direction = MakeResponsive({
    type t = [#row | #column | #"row-reverse" | #"column-reverse"]
  })
  //... and plenty of other props that I'm not listing here

 @module("@carla/flora") @react.component
  external make: ( 
    ~width: Width.t=?,
    ~maxWidth: Width.t=?,
     ~flex: Flex.t=?,
     ~display: Display.t=?,
     ~direction: Direction.t=?,
      //... and plenty of other props that I'm not listing here
    ) => React.element = "Box"

Here are some examples of how we can now use Box component

<Box width={100} />
<Box width={Box.Width.string("100%")} />
<Box maxWidth={Box.Width.responsive([
      Box.Width.string("100%"),
      Box.Width.skip,
      Box.Width.skip,
      200,
])} />
<Box flex={1}>
<Box direction={#row} />
<Box direction={Box.Direction.responsive([#column, Box.Direction.skip, #row])} />

You can see here that int is a default so you don’t have to do Box.Width.int(100) every time, only when it’s a string. This was a decision made inside the ResponsiveIntAndString module

If we’d only have ints and strings, this would maybe not safe us so many lines of code, but when we add values like the ones for Direction.t it’s really convenient. We’ve plenty of other parameters like this that we pass to the component, like justifyContent and other CSS attributes:

 module JustifyContent = MakeResponsive({
    type t = [
      | #"flex-start"
      | #"flex-end"
      | #center
      | #stretch
      | #"space-between"
      | #"space-around"
      | #"space-evenly"
    ]
  })

I hope this will help you writing more easily readable bindings :-)

If you’re curious you can read more about module functions (functors) here in Rescript’s docs.


Get more tips in my almost monthly newsletter about CSS & React!