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:
- Sanctuary knows about our type,
FirstType
, as it’s listed in the error message. S.add
‘s type signature isFiniteNumber -> 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.add
takes two FiniteNumber
s 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:
- 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. - 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.
- 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. - 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