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.
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.
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?
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.
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:
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:
a
is known. More a
's cannot be made out
of thin air.IO
monad.
In Ocaml this is not true.a
. In Haskell,
a
would have to be an instance of the Ord
typeclass for that to happen.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.
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.