Ocaml vs Maybe Not

In Rich Hickey's recent talk, Maybe Not, he argues various weaknesses of static types, using Haskell as an example, and the strengths of Clojure Spec. To sum up the talk, Hickey believes that requiring less and providing more should be possible to express easily.

For example, having the functions:

val f1 : int -> int
val f2 : int -> int option

and changing their types to be:

val f1 : int option -> int
val f2 : int -> int

Should not require modifying any call-site code.

Secondly, a function that takes aggregates should be able to accept anything that has at least the elements it require and return anything that has at least the elements it states. There is no Ocaml syntax to express this so some pseudo-code:

If a function has a type of:

val f : {name : string; age : int} -> {id : int; address : string}

The function f should be able to take any of the following inputs:

type r1 = { name : string; age : int; zip_code : string }
type r2 = { name : string; age : int; maiden_name : string }

And it should be able to return a record that has even more attributes in it than just the id and address.

In Ocaml it is possible to do some of these things but not all.

Loosening Function Inputs

Using Ocaml's keyword arguments it's possible to make a function that previous required a value and make it optional without changing the call-site. This looks like:

let f ~(id : int) () = id;;
(* val f : id:int -> unit -> int = <fun> *)

f ~id:10 ();;
(* - : int = 10 *)

let f ?(id : int option) () = match id with Some v -> v | None -> 0;;
(* val f : ?id:int -> unit -> int = <fun> *)

f ~id:10 ();;
(* - : int = 10 *)

f ();;
(* - : int = 0 *)

Note that for this to work, the function must take at least one non-optional parameter, that is why the function takes a value of type unit at the end.

Strengthening Function Outputs

This is not possible in Ocaml. From a static types perspective it's not quite clear what it means. Take the following code:

match f () with
  | Some v -> ...
  | None -> ...

If f is modified such that it always returns a value, named v in this case, what does that match mean?

Aggregates

Early in the talk the point is made that Clojure tends to pass maps around, which are sets of things, and these maps may have more things than a function cares about. While not common in Ocaml, this is possible using the Hmap library. This allows values of different types to be put into a map and retrieved in a type safe way.

But what Hickey describes here with schema and selection on records is a stronger guarantee than just getting a bag of values and returning a bag of values. It is possible to implement its functionality over Hmap, it is also possible to do what he wants for inputs by using objects in Ocaml.

Using his example, the functions get-movie-time and place-order can be defined as:

let get_movie_time (user : < id : int; addr : < zip : int; .. >; .. >) = failwith "not implemented";;
(* val get_movie_time : < addr : < zip : int; .. >; id : int; .. > -> 'a = <fun> *)

let place_order (user : < first : string
            ; last : string
            ; addr : < street : string; city : string; state : string; zip : string; .. >
            ; .. >) = failwith "not implemented";;
(* val place_order :
  < addr : < city : string; state : string; street : string; zip : string; .. >;
    first : string; last : string; .. > ->
  'a = <fun> *)

This is possible through row-polymorphism. There are limits on row-polymorphism that likely make it unable to express the exact same things as Clojure Spec. One thing to note is that Hickey makes a point of saying that being able to express these things as a tree is possible. The examples above show this is possible, the addr field is an object. The example above is also verbose for demonstration purposes, these types can be given names to not require writing them out like this.

The reverse is not possible, however, as there is no way for the compiler to know what extra stuff is in the object the function has returned. The function can always return an object with a lot of stuff in it but it must always return an object with that stuff.

Note that this only applies to values statically constructed in the program. One cannot take a data off the wire and construct an object that matches what's in it.

Commentary

The title and introduction focusing on the Maybe type is a bit of a red-herring as most of the talk is really focused on record types being too specific. While it is true that changing the input or output of a function that is backwards compatible might result in having to modify the call-sites, in the author's experience those situations:

  1. Do not happen often. It is rare that a function that previously might have returned a value now always returns a value without some larger change happening.
  2. Are found by the compiler and easy to change. They could even be automated with tooling as they tend to be mechanical.

The real value of the talk is about the shape of data as it flows through a system. Unfortunately in the talk, Hickey compares type systems and Clojure Spec as if they are equivalent, but they aren't. While they may be used to solve a similar kind of problem, what they tell the programmer is different. The suggested way of doing things in Clojure, of passing maps around, is possible in Ocaml using Hmap. And running validation functions over the inputs and outputs of a function is also possible. Maybe Ocaml should make it easier to work with Hmap but maybe Ocaml developers want to know different things about their program.

It's likely that Hickey gets a lot of pro-static type people who insist that static types are The One True Way and it's unfortunate that this talk (and previous ones) seems to be using them as the counter perspective. A language like Ocaml does struggle with having different views on the same data. However an Ocaml programmer might be okay with that cost relative to how Clojure Spec works. Like a lot of things in programming: it depends on what one values.

At the end of the talk Hickey makes a comment about how the type of the reverse ([a] -> [a]) function is useless. But this is a statement of one's preferences wrapped in an argument about utility. That type does say some things that a static typist finds useful:

  1. The function must return the input list or a subset of it. This is because nothing about the type variable a is known. More a's cannot be made out of thin air.
  2. In Haskell, the function does no IO, otherwise it would be in the IO monad. In Ocaml this is not true.
  3. It does not apply any specific ordering to the values. It can't. It has no way of comparing them since it doesn't know anything about a. In Haskell, a would have to be an instance of the Ord typeclass for that to happen.
  4. This is a static attribute of the program. It's not possible that every 10000th run it returns an int.

A lot of the information a type definition gives a user is about what the value cannot do rather than what it can. Hickey never brings this up. Maybe he does not value it or believes that Clojure Spec does that better. But Clojure Spec does not tell the user irrefutable truths about a program. Again, that is important to some people and less important to others.

Conclusion

Some of the concepts that Hickey brings up in this talk are possible in Ocaml to some degree, but probably not to the degree that Clojure Spec allows. His argument around aggregates is a deficiency of static type systems and while it is possible to accomplish in a statically typed language it may not be practical. It also might have other costs in terms of the static guarantees of the program. Whether or not being able to express these things is important depends on the programmer.