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 option
s and
list
s: 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.