(* Deriving the context-passing implementation of polyvariadic printf,
Section 5.2 of the paper.
*)
(* We start with the refined tupling implementation of printf in Fig 5,
the descP3 implementation.
The following is a brief summary.
*)
(* Constructors for the argument sequence *)
let nilA3 = ()
let consA3 h t = (h,t)
;;
(* Deconstructors for the argument sequence *)
let is_nilA3 = fun () -> ()
let un_consA3 x = x
;;
(* Constructors for the result, string sequence: *)
let nilS3 = ""
let consS3 h t = h ^ t
;;
(* Constructors for the descriptor sequence *)
(* val nilDP3 : unit -> string *)
let nilDP3 = fun x -> is_nilA3 x; nilS3
(* val consDP : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c *)
let consDP3 d tD = fun x -> d tD x (* identity *)
;;
(* Primitive format descriptors *)
let litP3 str = fun tD arg ->
consS3 str (tD arg)
;;
(* val intP3 : ('a -> string) -> int * 'a -> string *)
let intP3 = fun tD arg ->
let (n,l2) = arg in
consS3 (string_of_int n) (tD l2)
;;
(* val charP3 : ('a -> string) -> char * 'a -> string *)
let string_of_char x = String.make 1 x;;
let charP3 = fun tD arg ->
let (c,l2) = arg in
consS3 (string_of_char c) (tD l2)
;;
let printf3 = fun x -> x
;;
(* The running example *)
let descP3 = consDP3 intP3
(consDP3 (litP3 "-th character after ")
(consDP3 charP3
(consDP3 (litP3 " is ")
(consDP3 charP3
nilDP3))))
;;
(* val descP3 : int * (char * (char * unit)) -> string = *)
printf3 descP3 (5,('a',('f',())));;
(*
- : string = "5-th character after a is f"
*)
(* We now choose a slightly different representation for the string
sequence. It is no longer a string. Rather, it is a
function that receives a string (an accumulator) and appends to it.
In short, the S-sequence is now a difference string. In the optimized
implementation, the accumulator could be a list of string fragments
accumulated in reverse order; Building of the S-sequence would
no longer need any string concatenations and can be done in
linear time (with respect to the length of the sequence). We defer
such an optimization to a later time, however.
*)
let nilSC4 = fun acc -> acc;;
let consSC4 h t = fun acc -> t (acc ^ h)
;;
(* We re-write our implementation to use nilSC4, consSC4 *)
let nilDPC4 = fun x -> is_nilA3 x; nilSC4
let consDPC4 = consDP3 (* remains the same, identity *)
;;
(* When we re-write litP, intP, charP, we inline consSC4 *)
let litPC4 str = fun tD arg acc ->
(tD arg) (acc ^ str)
;;
(* val intPC4 : ('a -> string -> 'b) -> int * 'a -> string -> 'b *)
let intPC4 = fun tD arg acc ->
let (n,l2) = arg in
(tD l2) (acc ^ (string_of_int n))
;;
let charPC4 = fun tD arg acc ->
let (c,l2) = arg in
(tD l2) (acc ^ (string_of_char c))
;;
(* Let us look at the bigger picture of that change. In Sec 2, we have
derived the general implementation for consD and nilD, which we have
been specializing since:
let consD dP tD = fun (y,l2) -> consS (dP y) (tD l2)
let nilD = fun () -> nilS
If we inline nilSC4 and consSC4 here, we obtain
let consD dP tD = fun (y,l2) acc -> tD l2 (acc ^ (dP y))
let nilD = fun () acc -> acc
Thus a descriptor sequence is a function of two arguments: it takes
an argument sequence and the accumulator.
Let us flip the argument order. That may sound a bit arbitrary, but we shall
see the reasons for that: to bring the arguments in the right
order for eta-reduction. But first, let's examine the consequences
of such a change:
let consD dP tD = fun acc (y,l2) -> tD (acc ^ (dP y)) l2
let nilD = fun acc () -> acc
The descriptor sequence now takes the accumulator as the first argument.
We can also view this sequence of transformations -- introducing an
accumulator and switching the order of arguments -- as fusing the call
of tD with a bit of its context, (consS formatted_value []).
We should recall printf. Before, it was the identity. We have changed
the string sequence to be a difference string. We still want printf to
return an ordinary string. Thus we have to change printf to pass the
empty string as the accumulator. The flip in the argument order makes
the accumulator the first argument of the descriptor sequence, so that
new printf becomes
let printfC5 desc args = desc "" args
or, eta-reduced
*)
let printfC5 desc = desc "";;
(* we should flip the argument order -- put acc ahead of arg --
in the other places
*)
let nilDPC5 = fun acc x -> is_nilA3 x; acc
let consDPC5 = consDP3 (* remains the same, identity *)
;;
(* Recall that tD is the rest of the descriptor sequences; that is,
tD is a function whose argument order we have just flipped.
*)
let litPC5 str = fun tD acc arg ->
tD (acc ^ str) arg
;;
(* val intPC4 : ('a -> string -> 'b) -> int * 'a -> string -> 'b *)
let intPC5 = fun tD acc arg ->
let (n,l2) = arg in
tD (acc ^ (string_of_int n)) l2
;;
let charPC5 = fun tD acc arg ->
let (c,l2) = arg in
tD (acc ^ (string_of_char c)) l2
;;
(* The function litPC5 clearly shows the benefit of the flip: we can eta-reduce
the arg argument:
*)
(* val litPC6 : string -> (string -> 'a) -> string -> 'a *)
let litPC6 str = fun tD acc ->
tD (acc ^ str)
;;
(* The function intPC5 can be re-written simply as
let intPC5 = fun tD acc (n,l2) ->
tD (acc ^ (string_of_int n)) l2
;;
We can curry it, and eta-reduce l2:
*)
(* val intPC6 : (string -> 'a) -> string -> int -> 'a *)
let intPC6 = fun tD acc n ->
tD (acc ^ (string_of_int n))
;;
(* and the same for charPC6 *)
let charPC6 = fun tD acc c ->
tD (acc ^ (string_of_char c))
;;
let nilDPC6 = nilDPC5
let consDPC6 = consDP3 (* remains the same, identity *)
;;
(* The running example becomes *)
let descPC6 = consDPC6 intPC6
(consDPC6 (litPC6 "-th character after ")
(consDPC6 charPC6
(consDPC6 (litPC6 " is ")
(consDPC6 charPC6
nilDPC6))))
;;
(* val descPC6 : string -> int -> char -> char -> unit -> string *)
(* The type shows that we have `uncurried' the tuple! *)
printfC5 descPC6 5 'a' 'f' ();;
(*
- : string = "5-th character after a is f"
*)
(* We can trace the final () to is_NilA3 in nilDPC5. The function nilDPC5
does not have to take any argument x, which is dummy () anyway.
After removing the argument, see see that
let nilDPC7 = fun acc -> acc
nilDP becomes the identity.
Since consDP is the identity, we drop it. The descriptor sequence thus
becomes
let descP = intP
((litP "-th character after ")
(charP
((litP " is ")
(charP
nilDP))))
;;
which is just the functional composition of primitive descriptors applied
to nilDP, which is the identity function.
We thus obtain the following final implementation.
*)
(* val printfC7 : (('a -> 'a) -> string -> 'b) -> 'b *)
let printfC7 desc = desc (fun x -> x) "";;
(* Primitive format descriptors *)
(* val litPC7 : string -> (string -> 'a) -> string -> 'a *)
let litPC7 str = fun tD acc ->
tD (acc ^ str)
;;
(* intPC7 : (string -> 'a) -> string -> int -> 'a *)
let intPC7 = fun tD acc n ->
tD (acc ^ (string_of_int n))
;;
(* charPC7 : (string -> 'a) -> string -> char -> 'a *)
let charPC7 = fun tD acc c ->
tD (acc ^ (string_of_char c))
;;
(* Functional composition: infix operation *)
(* val ( ^^ ) : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b *)
let (^^) f g = fun x -> f (g x);;
(* The running example becomes *)
let descPC7 =
intPC7 ^^ (litPC7 "-th character after ") ^^ charPC7 ^^ (litPC7 " is ") ^^ charPC7
;;
(* val descPC7 : (string -> '_a) -> string -> int -> char -> char -> '_a *)
printfC7 descPC7 5 'a' 'f';;
(*
- : string = "5-th character after a is f"
*)
(* We have thus derived Danvy's functional unparsing. *)