Haskell has a function fmap which can map over a number of different
datatypes. For example, fmap can map a function over both a List and a
Maybe (the equivalent of an option in OCaml):
Prelude> fmap (+ 1) [1,2]
[2,3]
Prelude> fmap (+ 1) (Just 3)
Just 4
Unfortunately, the equivalent is impossible in OCaml. That is, there’s no way to
define an OCaml value fmap so that the two expressions:
# fmap [1;2] ~f:((+) 1)
# fmap (Some 3) ~f:((+) 1)
both typecheck and evaluate to the right value.
Even if we eliminate the complexity of type inference by specifying the type
explicitly, we can’t define fmap so that the two expressions:
# fmap ([1;2] : _ list) ~f:((+) 1)
# fmap (Some 3 : _ option) ~f:((+) 1)
typecheck and evaluate to the right value.
However, the Generic module in Jane Street’s Core_extended library will let
us do exactly that with just a trivial syntactic change. But before continuing,
I’ll warn you that the Generic module is not necessarily something you’d want
to use in real world code; it falls much more in the “cute trick” category. But
with that caveat, let’s look at our example using Generic:
# open Core.Std;;
# open Core_extended.Generic;;
# map ([1;2] >: __ list) ~f:((+) 1);;
- : int list = [2; 3]
# map (Some 3 >: __ option) ~f:((+) 1);;
- : int option = Some 4
Note that, after opening the Generic module, all we did to the previous
example was change : to >: and _ to __. (Also, the Generic module
calls the mapping function map instead of fmap, but that’s inconsequential.)
Of course, the trick is that >:, __, list, and option are actually
values defined by the Generic module in such a way that their intended usage
looks like a type annotation.
Note that these “types” are nestable as you would expect real types to be:
# map ([None; Some 3] >: __ option list) ~f:((+) 1);;
- : int option list = [None; Some 4]
This means that you can change what map does just by changing the “type” you assign to its argument:
# map ([None; Some 3] >: __ option list) ~f:(fun _ -> ());;
- : unit option list = [None; Some ()]
# map ([None; Some 3] >: __ list) ~f:(fun _ -> ());;
- : unit list = [(); ()]
The Generic module also defines a generic fold function so that you can
accumulate values at any “depth” in your value:
# fold ([[Some 3; None]; [Some 5; Some 2]] >: __ option list list) ~init:0 ~f:(+);;
- : int = 10
Not every “type” formable is __ followed by some sequence of options and
lists: for example, Generic also provides string (considered as a
container of characters):
# map ([Some "foo"; None; Some "bar"] >: string option list) ~f:Char.uppercase;;
- : string option list = [Some "FOO"; None; Some "BAR"]
Note that the fact that the “types” are nestable means that these values must
have unusual definitions: in particular, __ (and string) are functions which
must be able to take a variable number of arguments. Indeed, these values are
defined using a technique sweeks wrote about in
a blog post on variable argument functions: the
f and z in sweeks’s post are analogous here to __ and >: respectively.
Here’s the definition of the primitive values we’ve used so far (Generic
actually defines a few more):
let __ k = k (fun f x -> f x)
let ( >: ) x t y = t (fun x -> x) y x
let map x ~f = x f
let string k = k (fun f -> String.map ~f)
let list map k = k (fun f -> List.map ~f:(map f))
let option map k = k (fun f -> Option.map ~f:(map f))
The types of these turn out to be extremely unhelpful, and you can’t really use
them to figure out how to use these values. For example, here is the type of
>: (and this isn’t just the inferred type of the above definition, this is the
type which must actually be exposed to use >:):
val ( >: ) : 'a -> (('b -> 'b) -> 'c -> 'a -> 'd) -> 'c -> 'd
Finally, is this module actually used? The answer is no. As far as I know, it’s used nowhere in Jane Street’s codebase. But it’s still kind of cute.