type Maybe a = Just a | Nothing
07 April 2016
Tags: elm javascript
TweetIf you have worked with JavaScript (or quite a few other languages that embrace null) I bet you have had one or two errors that can be traced back to an unexpected null reference. Some of them are obvious, but others are really tricky to track down. I’m sure most of you are well aware that quite a few other languages banishes null and introduces a Maybe or Option type to handle nothingness. Elm is one of those languages. Before I started looking at Elm I hadn’t really worked with Maybe types. In this blogpost I thought I’d share a little more insight on how to work with them in Elm. I’ll also briefly cover how they might be (or not) used in JavaScript for reference.
Elm is a statically typed language which compiles down to JavaScript. Types is a core ingredient of Elm, that’s not the case with JavaScript obviously. |
type Maybe a = Just a | Nothing
The Maybe type in Elm looks deceivingly simple. And actually it is.
The type is parameterized and the a
is a placeholder for a concrete type in your program.
So a
here means any type (Int, String, Float etc). A Maybe can have one of two values; either Just
some value of type a
or it is Nothing
.
Where does Just
and Nothing
come from ? Are they defined somewhere else ? They are part of the type definition, think of them as tags. The name of these "tags"
must start with an upper case letter in Elm.
x = Just 0.0 -- Just 0.0 : Maybe.Maybe Float (1)
y = Nothing -- Nothing : Maybe.Maybe a (2)
1 | The variable x Maybe with the tag Just and the Float value 0.0 (Maybe lives in a namespace or rather module in Elm called Maybe, that’s why the actual type definitions states Maybe.Maybe) |
2 | The variable y becomes a Maybe with the tag Nothing. Nothing has no value, and hence no value type associated. Nothing is Nothing, but it’s still a Maybe though :-) |
Elm is a statically typed language, everything is represented through types. So before we carry on I’d like to briefly cover the concept of type annotations.
Since JavaScript doesn’t have types, I’ll use Java as a comparable example
public int increment(int value) {
return value++;
}
public int add (int x, int y) {
return x + y;
}
increment : Int -> Int (1)
increment value =
value + 1
add : Int -> Int -> Int (2)
add x y =
x + y
1 | The type annotation for increment tells us it is a function which takes an argument of type Int and returns an Int |
2 | add takes two arguments of type Int and returns a an Int . So think of the last one as return type. |
Type annotations in Elm are optional, because the compiler is able to infer the types statically. Most people tend to use type annotations because they provide very useful documentation. When working with Elm it’s really something you quickly have to learn, because most documentation will use them and the Elm compiler will most certainly expose you to them.
Ok so I have this maybe thing which can be a Just some value or Nothing. But how do I get hold of the value so I can work with it ?
myList : List String (1)
myList = ["First", "Second"] (2)
-- List.head : List a -> Maybe.Maybe a (3)
case List.head myList of (4)
Nothing -> (5)
"So you gave me an empty list!"
Just val -> (6)
val
-- returns "First"
1 | Type annotation for myList. It is a List of String. It’s just a value, so that’s why there is no arrows in the type annotation |
2 | We are using a list literal to define our list. Each list item must be separated by a comma. It’s also worth noting, that every item in the list must be of the same type. You can’t mix Strings with Ints etc. The Elm compiler will yell at you if you try |
3 | I’ve added the type annotation for the List.head function. Given a List of values with type a it will return a Maybe of type a . List.head returns the first item of a List. The reason it returns a Maybe is because the List might be empty. |
4 | You can think of case as a switch statement on stereoids. Since List.head return a Maybe we have to possible case’s we need to handle |
5 | In this instance we can see from the code this case will never happen, we know myList contains items. The Elm compiler is really smart, but not that smart so it doesn’t know the list is empty. |
6 | This case unwraps the value in our Just so that we can use it. We just return the value, which would be "First". The value is unwrapped using something called pattern matching. In JavaScript terms you might think of it as destructuring |
The Maybe type is defined in a module called Maybe
. In addition to the Maybe type it also includes a collection
of handy functions that makes it handy to work with Maybe types in various scenarios.
myList = ["First", "Second", "Third"]
first = List.head myList
second = List.head (List.drop 1 myList)
tail = List.tail myList -- Just ["Second","Third"] : Maybe (List String)
-- Maybe.withDefault : a -> Maybe a -> a (1)
Maybe.withDefault "No val" first -- -> "First" (2)
Maybe.withDefault "No val" (List.head []) -- -> "No val"
1 | Maybe.withDefault takes a default value of type a a Maybe of type a . It returns the value of the maybe if it has a value (tagged Just ) otherwise it returns the provided default value |
2 | In the first example first is Just "First" so it unwraps the value and returns that. In the second example there is no value so it returns the provided default |
-- Maybe.map : (a -> b) -> Maybe a -> Maybe b (1)
Maybe.map String.toUpper first -- -> Just "FIRST" (2)
Maybe.map String.toUpper Nothing -- -> Nothing
-- Maybe.map2 (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c (3)
Maybe.map2 (\a b -> a ++ ", " b) first second -- -> Just "First, Second" (4)
Maybe.map2 (\a b -> a ++ ", " b) first Nothing -- -> Nothing
Maybe.map2 (++) first second -- -> Just "First, Second" (5)
1 | Maybe.map takes a function which has the signature (a → b), that means a function that takes any value of type a and return a value of type b (which can be the same type or a completely different type). The second argument is a Maybe (of type a ). The return value is a Maybe of type b . So Maybe.map unwraps the second argument, applies the provided function and wraps the result of that in a Maybe which in turn is returned. |
2 | String.toUpper takes a String (a if you like) and returns a String (b if you like). String.toUpper doesn’t understand Maybe values, so to use it on a Maybe value we can use Maybe.map |
3 | Maybe.map2 is similar to Maybe.map but the function in the first argument takes two in parameters. In addition to the function param we provide two Maybe values. These two doesn’t need to be of the same type, but happens to be so in our example. There is also map3, map4 etc up to map8 |
4 | If any or both of the two Maybe params are Nothing the result will be Nothing. |
5 | In the example above we used an anonymous function (lambda). However ++ is actually a function that takes two arguments so we can use that as the function argument |
-- Maybe.andThen Maybe.Maybe a -> (a -> Maybe b) -> Maybe b (1)
Maybe.andThen tail List.head -- -> Just "Second" (2)
tail `Maybe.andThen` List.head -- -> Just "Second" (3)
tail
`Maybe.andThen` List.head
`Maybe.andThen` (\s -> Just (String.toUpper s)) -- -> Just "SECOND" (4)
Just []
`Maybe.andThen` List.head
`Maybe.andThen` (\s -> Just (String.toUpper s)) -- -> Nothing (5)
1 | Maybe.andThen resembles Maybe.map but there are two vital differences. The function argument comes as the second param (we’ll come back to why), secondly the function in the function argument must return a Maybe rather than a plain value. |
2 | The first argument tail is a Maybe, the second argument is List.head which is a function that takes a list as an argument and returns a Maybe, so that conforms to the function params signature required by Maybe.andThen |
3 | In this version we use the infix version of andThen (marked by backticks before and after). This is the reason the function argument comes second, so you typically use Maybe.andThen when you you need to work with maybes in a pipeline sort of fashion. |
4 | This is an example of piping values when dealing with Maybe values. We start with the tail of our list and then we pick out the head of that list and then we convert the value of that to uppercase |
5 | You can almost think of andThen as a callback. If any step of the chain returns Nothing, the chain is terminated and Nothing is returned |
type Perhaps a = Absolutely a | NotSoMuch
Of course interop with others will be an issue and Maybe
has some advantages being part of the core library. But still
if you really really want to…
function headOfList(lst) {
if (lst && lst.length > 0) {
return lst[0];
} else {
// hm... not sure. let's try null
return null;
}
}
function tailOfList(lst) {
if (lst && lst.length > 1) then
return lst.slice(0);
} else {
// hm... not sure. let's try null
return null;
}
}
var myList = ["First", "Second", "Third"];
var first = headOfList(myList); // "First"
var second = headOfList(tailOfLIst(myList)) // "Second"
var tail = tailOfList(lst); // ["First", "Second"]
first // "First"
headOfList([]) // null (1)
first.toUpperCase() // "FIRST"
headOfList([]).toUpperCase() // Type Error: Cannot read property 'toUpperCase' of null (2)
first + ", " + second // "First, Second"
first + ", " + null // "First, null" (3)
headOfList(tail).toUpperCase() // "SECOND"
headOfList([]).toUpperCase() // Type Error: Cannot read property 'toUpperCase' of null (4)
1 | An empty list obviously doesn’t have a first item. |
2 | If this was in a function you might guard against this. But what would you return ? Would you throw a exception ? |
3 | Doesn’t look to cool, so you would have to make sure you guarded against this case. Let’s hope you tested that code path, otherwise it’s lurking there waiting to happen ! |
4 | Same as 2 |
Okay so most of this cases are pretty silly, we would have to come up with something more real life with functions calling functions calling functions etc. The bottom line is that you have to deal with it, but it’s up to you all the time to make sure nulls or undefined doesn’t sneak in. In most cases there are simple non verbose solutions to deal with them, but it’s also quite easy to miss handling them. If you do it can sometimes be quite a challenge tracking down the root cause.
It’s undoubtably a little more ceremony in Elm, but in return you will not ever get nullpointer exceptions.
If you are from a JavaScript background the blogpost Monads in JavaScript gives you a little hint on how you could implement Maybe in JavaScript.
Let’s borrow some code from there and see how some of the examples above might end up looking
function Just(value) {
this.value = value;
}
Just.prototype.bind = function(transform) {
return transform(this.value);
};
Just.prototype.map = function(transform) {
return new Just(transform(this.value));
};
Just.prototype.toString = function() {
return 'Just(' + this.value + ')';
};
var Nothing = {
bind: function() {
return this;
},
map: function() {
return this;
},
toString: function() {
return 'Nothing';
}
};
function listHead(lst) {
return lst && list.length > 0 ? new Just(lst[0]) : Nothing;
}
function listTail() {
return lst && list.length > 1 ? new Just(lst.slice[1]) : Nothing;
}
var myList = ["First", "Second", "Third"];
var first = listHead(myList);
var second = listTail(myList).bind(t => listHead(t));
var tail = listTail(myList);
// Similar to Maybe.map in Elm
first.map(a => a.toUpperCase()) // Just {value: "FIRST"} (1)
Nothing.map(a => a.toUpperCase()) // Nothing (object) (2)
// Similar to Maybe.map2 in Elm
first.bind(a => second.map( b => a + ", " + b)) // Just { value: 'First, Second' } (3)
first.bind(a => Nothing.map( b => a + ", " + b)) // Nothing (object)
// Similar to Maybe.andThen in Elm
tail.bind(a => listHead(a)).bind(b => new Just(b.toUpperCase())) // Just { value: 'SECOND' } (4)
new Just([]).bind(a => listHead(a)).bind(b => new Just(b.toUpperCase())) // Nothing (object) (5)
1 | first is a Just object. Since it has a value the arrow function is run as expected |
2 | When the value is Nothing (a Nothing object) toUpperCase is never run and the Nothing object is returned |
3 | In the arrow function of bind for first we ignore the unwrapped value and call map on second with a new arrow function which now has both the unwrapped value of both a and b. We concatenate the values and the map function ensures the result is wrapped up in a Just object If you remember the elm case for map2, that was a separate function. Here map is just a convenience to wrap up the innermost value in a Just. |
4 | tail is a Just object with the value ["First", "Second"] in the first level arrow function we pick out the head which returns a Just object with the value "Second". In the innermost arrow level function we do upperCase on the value and wrap in it a Just which is the end result. |
5 | We are starting with Just with a value of an empty array. In the first level arrow function we try to pick out the head of the list. Since that will return a Nothing object, Nothing passes straight through the second level arrow function, never executing the toUpperCase call. |
So as you can see it is possible to introduce the notion of Maybe in JavaScript. There are several libraries out there to choose from I haven’t really tried any of them. Regardless the issue you’ll be facing is that the other libraries you are using probably won’t be using your representation of Maybe if at all. But hey, maybe it’s better with something than nothing. Or whatever.
There is clearly a slight cost with explicitly handling nothingness everywhere. In Elm you basically don’t even have a choice. The type system and the compiler will force you into being explcit about cases when you don’t have a value. You can achieve the same as with null but you always have to handle them. In your entire program. The most obvious benefit you get, is that you simply will not get null reference related errors in Elm. When calling any function that accepts Maybe values as input params or return Maybe values you will be made well aware of that. The compiler will let you know, but typically you would also see type annotations stating this fact too. This explicitness is actually quite liberating once you get used to it.
In JavaScript you can try to be more explicit with nulls. You can even reduce the chances of null pointers ever happening by introducing a Maybe/Option like concept. Of course you wouldn’t introduce the possibility of null pointers in your code. However there’s a pretty big chance some bozo,responsible for one of the 59 libs you somehow ended up with from npm, have though.
There are plenty of bigger challenges than null pointer exceptions out there, but if you could avoid them altogether, surely that must a be of some benefit. I’ll round off with the obligatory quote from Tony Hoare as you do when one pays tribute to our belowed null.
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.