Provides N
-ary datatypes and operations.
open Base
open N_ary
The N_ary
library generalizes over the number of variant cases,
tuple parts, arguments, and other things the OCaml type system does
not allow first-class abstraction over. It's a handy answer to
questions like "do we have Either.t
but for three types?" or "why
do we have List.partition3_map
but not List.partition4_map
?"
For now, N_ary
generalizes enumerations, variants, tuples, and list
operations to values of N
in the range 2 to 16, inclusive. Over time
we may add more functionality or a larger range, although compilation
times are something like O(N^3)
in the maximum supported N
, so it
will never get too much larger.
The datatypes in this library are largely useful for glue code, but
not for more long-term or ubiquitous data representations. If your
debug log shows a transition from Case2 [ 19; 20; 21 ]
to Case4 "janestreet.com"
, those labels are not particularly self-documenting.
Instead, these types should persist for long enough to shuffle data
around, and then it can be put in a more semantically meaningful type.
For this reason, datatypes in N_ary
do not derive of_sexp
or
bin_io
, and do not provide stable serializations. If you need these
things, you should probably use a different data type.
Currently, we provide enumeration, variant, and tuple types.
The modules Enum2
through Enum16
define enumeration types with
the appropriate number of constructors, which we call cases.
type t = Enum3.t =
| Case0
| Case1
| Case2
Enumeration modules define constants:
# Enum3.case0
- : t = N_ary.Enum3.Case0
Enumeration modules define predicates:
# Enum3.is_case1 Case1
- : bool = true
# Enum3.is_case1 Case2
- : bool = false
Enumeration modules also define conversions to and from int
:
# Enum3.to_int Case2
- : int = 2
# Enum3.of_int_exn 2
- : t = N_ary.Enum3.Case2
The modules Variant2
through Variant16
define variant types with
the appropriate number of type parameters and constructors, which we
call cases.
type ('a, 'b, 'c) t = ('a, 'b, 'c) Variant3.t =
| Case0 of 'a
| Case1 of 'b
| Case2 of 'c
Variant modules define constructors:
# Variant3.case0 "hello"
- : (string, 'a, 'b) t = N_ary.Variant3.Case0 "hello"
Variant modules define predicates:
# Variant3.is_case1 (Case0 "hello")
- : bool = false
# Variant3.is_case1 (Case1 [ "good"; "bye" ])
- : bool = true
Variant modules define accessors:
# Variant3.get_case2 (Case1 `A)
- : 'a option = Base.Option.None
# Variant3.get_case2 (Case2 `B)
- : [> `B ] option = Base.Option.Some `B
Variant modules define a map
function over all cases:
# let f = Variant3.map ~f0:Int.succ ~f1:List.rev ~f2:String.uppercase
val f : (int, 'a list, string) t -> (int, 'a list, string) t = <fun>
# f (Case0 100)
- : (int, 'a list, string) t = N_ary.Variant3.Case0 101
# f (Case2 "lorem ipsum")
- : (int, 'a list, string) t = N_ary.Variant3.Case2 "LOREM IPSUM"
Variant modules define map_case*
functions for each case:
# Variant3.map_case0 ~f:Int.succ (Case0 100)
- : (int, 'a, 'b) t = N_ary.Variant3.Case0 101
# Variant3.map_case0 ~f:Int.succ (Case2 "lorem ipsum")
- : (int, 'a, string) t = N_ary.Variant3.Case2 "lorem ipsum"
Modules Tuple2
through Tuple16
define tuple types with appropriate
numbers of type parameters and values, which we call parts.
type ('a, 'b, 'c) t = ('a, 'b, 'c) Tuple3.t
constraint ('a, 'b, 'c) t = 'a * 'b * 'c
Tuple modules define constructors:
# Tuple3.create 1 2 3
- : (int, int, int) t = (1, 2, 3)
Tuple modules define accesssors:
# Tuple3.part0 ('a', 'b', 'c')
- : char = 'a'
Tuple modules define functional updaters:
# Tuple3.set_part1 ('a', 'b', 'c') 'B'
- : (char, char, char) t = ('a', 'B', 'c')
Tuple modules define a map
function over all parts:
# Tuple3.map ("one", "two", "three")
~f0:String.lowercase
~f1:String.capitalize
~f2:String.uppercase
- : (string, string, string) t = ("one", "Two", "THREE")
Tuple modules define map_part*
functions for each part:
# Tuple3.map_part1 ~f:String.uppercase ("one", "two", "three")
- : (string, string, string) t = ("one", "TWO", "three")
N_ary
provides generalized operations on lists.
The modules List2
through List16
provide versions of the
partition
, unzip
, zip
, map
, and iter
operations that produce
or consume the appropriate number of lists.
N-ary partitions generalize List.partition_tf
and List.partition_map
.
# List3.partition_enum (String.to_list "abc123.!?") ~f:(function
| 'a' .. 'z' | 'A' .. 'Z' -> Case0
| '0' .. '9' -> Case1
| _ -> Case2)
- : char list * char list * char list =
(['a'; 'b'; 'c'], ['1'; '2'; '3'], ['.'; '!'; '?'])
# List3.partition_map (String.to_list "ABCdef123") ~f:(function
| 'A' .. 'Z' as c -> Case0 (Char.lowercase c)
| 'a' .. 'z' as c -> Case1 (Char.uppercase c)
| c -> Case2 c)
- : char list * char list * char list =
(['a'; 'b'; 'c'], ['D'; 'E'; 'F'], ['1'; '2'; '3'])
N-ary zip and unzip operate on N lists and N-ary tuples.
# List3.unzip [ (1, 2, 3); (4, 5, 6) ]
- : int list * int list * int list = ([1; 4], [2; 5], [3; 6])
# List3.zip_exn [ 1; 4 ] [ 2; 5 ] [ 3; 6 ]
- : (int * int * int) list = [(1, 2, 3); (4, 5, 6)]
N-ary map operates on N lists at a time.
# List3.map_exn [ 100; 200 ] [ 30; 40 ] [ 5; 6 ] ~f:(fun a b c -> a + b + c)
- : int list = [135; 246]
# List3.mapi [] [ 1 ] [ 2; 3 ] ~f:(fun _ _ _ -> assert false)
- : 'a list List.Or_unequal_lengths.t =
Base.List.Or_unequal_lengths.Unequal_lengths
N-ary iteration operates on N lists at a time.
It is worth noting that side effects in ~f
behave differently in
iter
than iter_exn
. The former checks for unequal lengths up front;
the latter iterates first, and only raises at the end. This distinction
applies to all map*
and iter*
functions. See the relevant .mli
files
for details.
# List3.iter_exn [ 1; 2 ] [ 3; 4 ] [ 5; 6; 7 ] ~f:(fun x y z ->
Stdio.printf "%d%d%d\n%!" x y z)
135
246
Exception: "N_ary.List3.iter_exn: lists have unequal lengths"
# List3.iter [ 1; 2 ] [ 3; 4 ] [ 5; 6; 7 ] ~f:(fun x y z ->
Stdio.printf "%d%d%d\n%!" x y z)
- : unit List.Or_unequal_lengths.t =
Base.List.Or_unequal_lengths.Unequal_lengths