Defining Custom Types in Sanctuary

Sanctuary is a functional programming library for JavaScript that provides typechecking and compatibility with the Fantasy Land spec. If all that is a bit unfamiliar to you, never fear! Sanctuary’s docs provide a less esoteric way of describing the library:

Sanctuary promotes programs composed of simple, pure functions. Such programs are easier to comprehend, test, and maintain – they are also a pleasure to write.

Sanctuary lovingly wraps JavaScript in a run-time type system, with support for the usual suspects like Boolean, Int, String, etc. It also has a couple of ADTs like Maybe and Either.

The more I used Sanctuary, the more I started to bump into mentions of the ability to add your own custom types to its type system. Fluture, for example, has a separate repository for this.

I was intrigued. Could I, too, add my own twisted creations to Sanctuary and make it typecheck for me?

Buttering up

A common gripe with vanilla JavaScript is that it’s… a little too nice when it comes to type coercion. For example, I can do this:

const result = 42 + "butter"

and JavaScript will gleefully add a string and an integer value. The result, if you’re morbidly curious, is the string "42butter". This is admittedly a neat trick, and hey, maybe your application’s entire purpose is to add integers to butter! Who am I to get in the way of your business requirements?

The problem is this sort of flexibility can easily lead to unintended consequences as your app begins to get more complicated. Here’s a more subtle example:

const someUser = {
  id: 3,
  name: "Eric",
  email: "me@dokidoki.whatever"
}

const someProduct = {
  id: 123,
  name: "Exciting consumer item!"
}

const updateProductName = 
  (product, newName) => {
    return Object.assign(
      {}, 
      { ...product },
      { name: newName }
    )
}

// pass in user instead of product...
// ... aaaand it works. Kind of.

const newProduct = updateProductName(
  someUser,
  "Suggestive Refrigerator Magnet"
)

What happens here? Well, you end up with an object that looks like this:

{
  id: 3,
  name: 'Suggestive Refrigerator Magnet',
  email: 'me@dokidoki.whatever'
}

So you either have a user who’s profile name is "Suggestive Refrigerator Magnet", or (arguably worse) you’ve got a product with an email field and in incorrect value for id that will almost certainly cause some hard-to-find errors further down the line.

This is where a library like Sanctuary can really help us out, by leveraging a type system to prevent such silly mistakes.

Base type system

Before we get too crazy, let’s look at how Sanctuary’s base type system works. Revisiting our earlier "42butter" example, we will swap out the standard JavaScript + operator for Sanctuary’s S.add function.

// In the Node REPL
> const S = require('sanctuary')
> S.add(42)("butter")

TypeError: Invalid value
 add :: FiniteNumber -> FiniteNumber -> FiniteNumber
  ^^^^^^^^^^^^
  1

 1) "butter" :: String
 The value at position 1 is not a member of ‘FiniteNumber’.

It turns out that “butter”, much as it may want to, cannot become a number. Programming is rife with disappointments.

But this is good news from a type perspective! Whereas the native + operator is unaware of our intentions and so must play fast and loose with types, resulting in weird behavior, S.add will only let us use it if we play by the rules of the type system and give it FiniteNumbers as operators.

FiniteNumber?

Our error message above, in addition to saving us from trying to add numbers to butter, has also given us a peek into one of the types that Sanctuary uses: FiniteNumber. Digging through the source on the Sanctuary repo itself yields references to this only in type signatures and the like. To see the definition itself, we’ll have to look at another repository: sanctuary-def.

sanctuary-def is where the type system itself lives, and where FiniteNumber and friends are actually defined. So in our quest to write our own types, it may be instructive. This is the definition for FiniteNumber:

var FiniteNumber = NullaryTypeWithUrl (
    'sanctuary-def/FiniteNumber',
    function(x) { return ValidNumber._test (x) && isFinite (x); });

That NullaryTypeWithUrl bit looks suspicious. Looking through more of the source, it turns out that’s just a convenience wrapper for the NullaryType function, which shows up in the documentation with the following type signature:

NullaryType :: String -⁠> String -⁠> (Any -⁠> Boolean) -⁠> Type

So FiniteNumber, our type, is created via this NullaryType function. Our arguments to this function are:

  1. String: The name of the type. This is for printing to the console for errors and such.
  2. String: The url of the types documentation
  3. (Any -> Boolean): A function that determines whether or not a value belongs to this type. For example, if this were a type for positive numbers, we would check whether the value was greater than zero and return true if so, or false otherwise.

So really the meat of the thing is the third argument, which is going to allow the type system to tell us whether or not the values we’re using are of the type that it expects.

Defining Our Own Nullary Type

Without getting into the weeds about what exactly is Nullary about this Type, we should be able to use the formula above to create our own type, and get it added to Sanctuary.

Let’s start simple. I want to make a type whose entire membership is only the string "dokidoki".

const $ = require('sanctuary-def')

const FirstType = $.NullaryType
  ('dokidoki-types/FirstType')
  ('http://dokidokidriven.com/types/FirstType')
  ((x) => x === "dokidoki")

Walking through this:

const $ = require('sanctuary-def'): We need sanctuary-def‘s help with this one, so we’ll require it here. Convention is to assign this to a variable named $.

const FirstType = $.NullaryType: We create our type using the NullaryType constructor and assign it to a variable called FirstType

('dokidoki-types/FirstType'): For debugging purposes, we need a string version of the type name. Similar to the above where Sanctuary sort of namespaces its FiniteNumber type under sanctuary-def, we’ll call ours dokidoki-types/FirstType

('http://dokidokidriven.com/types/FirstType'): This is our (fake) URL for the documentation that we will, no doubt, add at our earliest convenience

((x) => x === "dokidoki"): We create a function that determines whether or not any provided argument is contained in our type. In this case, we only want the string "dokidoki", so the function is just a simple string comparison. If the argument is the string "dokidoki", we return true. Otherwise, we return false.

Again, we stated that our entire type is just a single string literal ("dokidoki"). If we attempt to use any other string, or indeed, any other value, Sanctuary will yell at us.

Adding FirstType to Sanctuary’s Environment

Simply creating the type doesn’t actually get us anywhere, since Sanctuary doesn’t know it exists automatically. We need to add the type to Sanctuary’s environment.

env is where all the types live, sort of like the typechecker’s list of valid types. If we load up Sanctuary and then do console.log(S.env), we’ll see a long list of entries like this:

[
...
_Type {
_test: [Function],
format: [Function: format],
keys: [],
name: 'sanctuary-def/FiniteNumber',
type: 'NULLARY',
types: {},
url: 'https://github.com/sanctuary-js/sanctuary-def/tree/v0.18.1#FiniteNumber' },
...
]

Ah ha! There’s our friend FiniteNumber from before! And it’s part of an array of similar entries. Now the question becomes, how do we get our FirstType added to that list?

It turns out that there are a couple of ways to include Sanctuary in our code. The simple way, which we used earlier, is what you’re probably used to seeing with other modules:

const S = require('sanctuary')

Nothing exciting there. But the documentation also mentions a second method:

const { create, env } = require('sanctuary')

const S = create ({
  checkTypes: true,
  env
})

Instead of requiring Sanctuary directly, we’re including two items from the libary: create and… env! After we’ve included those, we call create with two arguments.

The first argument tells Sanctuary whether or not to do run-time typechecking. We want to do that type-checking for our purposes, but if you’re in a production environment, generally this should be set to false for performance reasons.

The second argument is our new buddy env, which is the same env that we printed out before, with FiniteNumber in it. Chances are, if we’re able to add our FirstType to this env, which we established earlier is just an array, we can convince Sanctuary to typecheck for us. The documentation actually gives as an example of this, using the base concat method:

const $ = require('sanctuary-def')
const { create, env } = require('sanctuary')

const FirstType = $.NullaryType
  ('dokidoki-types/FirstType')
  ('http://dokidokidriven.com/types/FirstType')
  ((x) => x === "dokidoki")

const S = create({
  checkTypes: true,
  env: env.concat ([FirstType])
})

Now let’s print out env again and see if we can find FirstType in the list:

[
...
_Type {
_test: [Function],
format: [Function: format],
keys: [],
name: 'dokidoki-types/FirstType',
type: 'NULLARY',
types: {},
url: 'http://dokidokidriven.com/types/FirstType' }
...
]

There it is! It looks a lot like the definition from FiiniteNumber earlier, so it seems this was a success.

One Small Experiment

If Sanctuary is now aware of our FirstType type, it should be able to typecheck for us, right? Let’s revisit our example from before, when we tried to add 42 and "butter". We know that our FirstType is defined to be only the string "dokidoki", so let’s try doing something with that string and see if Sanctuary freaks out:

// In the REPL, with custom env
> S.add(42)("dokidoki")

TypeError: Invalid value
add :: FiniteNumber -> FiniteNumber -> FiniteNumber
^^^^^^^^^^^^
1
1) "dokidoki" :: String, FirstType
The value at position 1 is not a member of ‘FiniteNumber’.

Looks quite similar to our earlier message, with the only exception being the bit I’ve marked in blue: now Sanctuary is aware that the string "dokidoki" can be either of type String or of type FirstType!

Summary

Sanctuary has a run time typechecker that can help us notice when we’re doing something weird in JS, like adding strings and numbers together.

We can also define and add our own types to this type system, and Sanctuary will typecheck those custom types as well.

Errata

Versions used in this post:

  • sanctuary: 0.15.0
  • sanctuary-def: 0.18.1

Gist for the code in this post

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: