sig val blank : ? val binop : (int * int -> int) -> ? val unop : (int -> int) -> ? val const : int -> ? val eval : ? end
and implement it in a structure S such that the following makes sense:
open S (* expression with holes: [ ] + (~[ ] + (2 * [ ])) *) val f = eval (binop (op+) blank (binop (op+) (unop (op~) blank) (binop (op *) blank (const 2)))) val _ = print (Int.toString (f 100 2 3000)) (* prints 100 + (~2 + (2 * 3000)) = 6098 *) val _ = print "\n"
What's going on here is f builds up an expression tree out of binary and unary operators and integer constants and blank holes, calls eval on it, and this has the type
int -> int -> int -> int
which is a curried type with three integer arguments, equal to the number of occurrences of "blank" in the definition of f.
But how can you get eval to have a type that always has one curried argument for each blank, and in fact evaluates the right expression?
The essential trick is taken from some Janest code sean showed me as a hint:
I think it's a really cute cps-y thing to do. I think I remember this sort of thing being done to make a type-safe printf.
structure S :> sig type ('a, 'b) t val blank : (int -> 'a, 'a) t val binop : (int * int -> int) -> ('a, 'b) t -> ('b, 'c) t -> ('a, 'c) t val unop : (int -> int) -> ('a, 'b) t -> ('a, 'b) t val const : int -> ('a, 'a) t val eval : ('a, int) t -> 'a end = struct type ('a, 'b) t = (int -> 'b) -> 'a fun blank x = x fun binop f t1 t2 k = t1 (fn x => t2 (fn y => k (f (x, y)))) fun unop f t k = t (fn x => k (f x)) fun const n k = k n fun eval f = f (fn x => x) end structure T = struct open S val f = eval (binop (op+) blank (binop (op+) (unop (op~) blank) (binop (op * ) blank (const 2)))) val _ = print (Int.toString (f 100 2 3000)) val _ = print "\n" end