Defining type-checked functions in Sanctuary

In our previous post, we looked a little about how Sanctuary’s type system works, and created a simple type. We then added that type to Sanctuary’s env, so that it could use our type while type checking.

In order to confirm that everything was working, we tried adding a custom type, FirstType, to the number 42, using Sanctuary’s S.add function. And this was the result:

const x = 42
const s = "dokidoki"

S.add(x)(s)
TypeError: Invalid value

add :: FiniteNumber -> FiniteNumber -> FiniteNumber
                       ^^^^^^^^^^^^
                            1

1) "dokidoki" :: String, FirstType

The value at position 1 is not a member of ‘FiniteNumber’.

Based on the above response, we know two things:

  1. Sanctuary knows about our type, FirstType, as it’s listed in the error message.
  2. S.add ‘s type signature is FiniteNumber -> FiniteNumber -> FiniteNumber

On Type Signatures

If this is your first time encountering this syntax for type signatures, don’t panic! It’s actually pretty easy. The signatures are a list of types separated by arrows. The last item on the list is the return type of the function. Every other type listed is the type of one argument to the function.

Looking back at the signature for S.add:

FiniteNumber -> FiniteNumber -> FiniteNumber

We can see that S.addtakes two FiniteNumbers as arguments, and returns a third FiniteNumber as a result.

Note: Sanctuary’s functions are curried, so saying that S.add takes two arguments is incorrect, strictly speaking. If you’re unfamiliar with this, that’s ok! Go look up “function currying” if you’re curious. It’s not that hard to get your head around. For now though, let’s not get too into the weeds on this.

Bang Bang

While Sanctuary has a swell selection of functions available for us to use, we might want to leverage its typechecker for our own custom functions, just like we were able to do with custom types. For this, we’ll also be using sanctuary-def, just as we did when we made our custom types:

const $ = require('sanctuary-def')

const def = $.create({
  checkTypes: true,
  env: $.env
})

The first thing to do is create a def function. This is a utility function that accepts an environment of types and lets us define custom functions that can typecheck against those types. For our example, we’re going to use sanctuary-def‘s default environment.

As a simple example, let’s define a function that will accept a String, and replace every character in that string with the character !:

const bangBang =
  def ('bangBang')
      ({})
      ([$.String, $.String])
      (xs => {
        return xs
                 // convert the string to an array of chars
                 .split('')
                 // throw away the char and replace it with '!'
                 .map( _x => '!' )
                 // put the array back together into a string
                 .join('')
      })

def takes four arguments:

  1. The name of the function (in this case, bangBang), which will be used when printing type errors to the screen. This doesn’t have to be the same as the name of the variable that you assign the function to.
  2. This object contains any type-variable constraints that you want to impose on your types. This is outside of the scope of this particular article, so for now, we’ll just pass in an empty object.
  3. This is an array of types which essentially serves as our function signature. The signature in a more traditional format would be String -> String. Recall that the last type in a signature is the return type, and everything else is one argument. Our function takes one string as an argument, and returns a string as a result.
  4. Finally, this is the function implementation. For details on how we’re doing the actual character replacement, check out the comments in the code above.

Let’s give a quick test of our function to make sure it’s working correctly:

> console.log(bangBang("all the exclamations"))
!!!!!!!!!!!!!!!!!!!!

Looks good. But we also need to make sure that Sanctuary will typecheck for us. Since our function is expecting a string, let’s pass in a boolean value instead:

> console.log(bangBang(true))
Thrown:
TypeError: Invalid value

bangBang :: String -> String
            ^^^^^^
              1

1)  true :: Boolean
The value at position 1 is not a member of ‘String’.

Sanctuary points to the first type in bangBang‘s type signature, where a String is expected. Instead, it tells us, that we’ve given it a Boolean value, which won’t work. We’ve confirmed that Sanctuary is now doing the heavy lifting of typechecking any argument that we pass in to our function.

One more thing…

We saw above that Sanctuary will typecheck any arguments that we pass to our function. But what happens if our implementation is wrong? What if we tell Sanctuary that we’re going to return a String, but return a Boolean instead? Let’s change our implementation:

const bangBangBad =
  def ('bangBangBad')
      ({})
      ([$.String, $.String])
      (xs => true)

We’re throwing away the String argument xs, and just returning true. And when we try to run this function:

> bangBangBad("hello")
Thrown:
TypeError: Invalid value

bangBangBad :: String -> String
                         ^^^^^^
                           1

1)  true :: Boolean

The value at position 1 is not a member of ‘String’.

Sure enough, now the typechecker is pointing to second type in the signature, where it expects our return value to be of type String, but is instead seeing a Boolean.This tells us that Sanctuary will check both how we use our functions and how we implement them.

Summary

Just as we can define our own types in Sanctuary, we can also define functions and ask the typechecker to make sure our usage and implementations of those functions align with the types we specify.

Errata

Versions used in this post:

  • sanctuary: 1.0.0
  • sanctuary-def: 0.19.0

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: