(* More streamlined and condensed derivation of typed printf and scanf *) (* The specification The format descriptor, argument list and the result of printf are all sequences. That means they are built using `constructors' (consD,nilD), (consA, nilA) and (consS, nilS) correspondingly. The specification for printf is then printf nilD nilA = nilS printf (consD (lit str) desc) (consA () lst) = consS str (printf desc lst) printf (consD int desc) (consA x lst) = consS (str_of_int x) (printf desc lst) printf (consD char desc) (consA x lst) = consS (str_of_char x) (printf desc lst) The specification for scanf is dual: scanf nilD nilS = nilA scanf (consD (lit s) desc) (consS s lst) = consA () (scanf desc lst) scanf (consD int desc) (consS s lst) = consA (int_of_str s) (scanf desc lst) scanf (consD char desc) (consS s lst) = consA (char_of_str s) (scanf desc lst) This is the specification, not yet actual code. In particular, we still have to decide on the representation for consD, nilD, consA, etc. `constructors' and what it means to pattern-match on them. These `constructors' are not necessarily data constructors. We also leave aside the issue of typing, till the moment we fix the representation. *) (* First, we define *) let litP (str:string) () = str;; let intP x = string_of_int x;; let charP x = String.make 1 x;; let litS (str:string) inp = assert (str = inp); ();; let intS inp = int_of_string inp;; let charS inp = inp.[0];; (* We re-write the specification as follows then: printf nilD nilA = nilS printf (consD dP desc) (consA x lst) = consS (dP x) (printf desc lst) scanf nilD nilS = nilA scanf (consD dS desc) (consS s lst) = consA (dS s) (scanf desc lst) Again, this is still the specification, not the actual code. We have to choose the representation for nilD, consD, etc. We can chose them to be data constructors, but then we get a typing problem: the argument 'x' can be either unit, int or char. We'd like to avoid the universal type so that printf (consD int desc) (consA 'c' lst) would be a type error. We would like to try something like `tagless final' approach (so that consD, nilD are constructor functions rather than data constructors). Alas, at first blush, it does not seem to apply: tagless final can express `evaluation' that is a fold. But our printf is not fold: it is zipWith (with the restriction that two lists must have the same size). The function zipWith has the following specification. zipWith f nil nil = nil zipWith f (cons x l1) (cons y l2) = cons (f x y) (zipWith f x l2) In the case of printf and scanf, the 'f' function is the regular functional application. We can re-write the specification as follows: zipWith f nil = fun nil -> nil zipWith f (cons x l1) = fun (cons y l2) -> cons (f x y) ((zipWith f x) l2) (implicitly relying on some sort of pattern-matching in the argument of the function. We fix the exact form of that later.) and again zipWith f nil = fun nil -> nil zipWith f (cons x l1) = (fun x -> fun z -> fun (cons y l2) -> cons (f x y) (z l2)) x (zipWith f x) By the universality of foldr, we conclude that zipWith is foldr (see Graham Hutton, A Tutorial on the Universality and Expressiveness of Fold, JFP 1999). zipWith f l1 l2 = foldr f' z l1 l2 where f' x tD = fun (cons y l2) -> cons (f x y) (tD l2) z = fun nil -> nil In other words, the specification of printf can be expressed in terms of foldr: printf desc args = foldr f' z desc args where f' dP tD = fun (consA y l2) -> consS (dP y) (tD l2) z = fun nilA -> nilS This immediately leads to the following code: we choose printf = id tuple-encoding for the argument list and the result list: nilA = () consA x y = (x,y) ditto for consS, nilS which gives as the specification desc args = foldr f' z desc args where f' dP tD = fun (y,l2) -> ((dP y),(tD l2)) z = fun () -> () and the representation for nilD and consD *) let nilD () = ();; let consD d tD (y,l2) = ((d y),(tD l2));; let desc1 = consD intP (consD (litP "-th character after ") (consD charP (consD (litP " is ") (consD charP nilD))));; (* val desc1 : int * (unit * (char * (unit * (char * unit)))) -> string * (string * (string * (string * (string * unit)))) = *) (* printf is just the identity, which we elide *) let r = desc1 (5,((),('a',((),('f',())))));; (* val r : string * (string * (string * (string * (string * unit)))) = ("5", ("-th character after ", ("a", (" is ", ("f", ()))))) *) (* We can also chose nilS to be an empty string and consS to be string concatenation, so obtain the output as the familiar string. *) (* The case for scanf is nearly identical. We show the final result. descS1 is descP1 with intP replaced with intS, etc. *) let descS1 = consD intS (consD (litS "-th character after ") (consD charS (consD (litS " is ") (consD charS nilD))));; (* val descS1 : string * (string * (string * (string * (string * unit)))) -> int * (unit * (char * (unit * (char * unit)))) = *) descS1 r;; (* - : int * (unit * (char * (unit * (char * unit)))) = (5, ((), ('a', ((), ('f', ()))))) *) (* We observe that (printf descP) and (scanf descS) form an injection-projection pair (where descS is obtained from descP by replacing lit with litS, int with intS -- and vice versa). *) (* ======================================================================== Deriving typed scanf satisfying the interface of the format input built into OCaml. *) (* Recall that nilA and consA are defined as follows: *) let nilA = () let consA x y = (x,y) ;; (* We re-write nilD and consD in terms of de-constructors *) let is_nilS () = () let un_consS (h,t) = (h,t) ;; let nilDS = fun x -> is_nilS x; nilA let consDS d tD = fun x -> let (y,l2) = un_consS x in consA (d y) (tD l2) ;; (* We now move un_consS into the definition of primitive descriptors. Now consDS becomes: *) let nilDS = fun x -> is_nilS x; nilA let consDS d tD = fun x -> let (v,l2) = d x in consA v (tD l2) ;; (* Where the new primitive descriptors (denoted as d above) are defined as follows: *) let litS11 (str:string) = fun x -> let (y,l2) = un_consS x in assert (str = y); ((),l2) let intS11 = fun x -> let (y,l2) = un_consS x in (int_of_string y,l2) let charS11 = fun x -> let (y,l2) = un_consS x in (y.[0],l2) ;; let nilD () = ();; let consD d tD (y,l2) = ((d y),(tD l2));; (* val nilDS : string -> unit *) let nilDS = fun x -> is_nilS x; nilA (* val consDS : ('a -> 'b * 'c) -> ('c -> 'd) -> 'a -> 'b * 'd *) let consDS d tD = fun x -> let (vy,l2) = d x in consA vy (tD l2) ;; (* We fix the formatted sequence to be a string: nilS is the empty string and consS is the string concatenation. We fuse the string deconstruction with primitive scanf descriptors. The primitive scanf descriptors are now as follows: *) exception Scan_error of string ;; (* val litS2 : string -> string -> unit * string *) let litS2 str = fun inp -> if String.length str <= String.length inp && str = String.sub inp 0 (String.length str) then ((), String.sub inp (String.length str) (String.length inp - String.length str)) else raise (Scan_error "lit") ;; (* val intS2 : string -> int * string *) let intS2 = fun inp -> let n = String.length inp in let rec loop acc i = if i >= n then (acc,"") else let c = inp.[i] in if c >= '0' && c <= '9' then loop (acc * 10 + (int_of_char c - int_of_char '0')) (succ i) else if i = 0 then raise (Scan_error "int") else (acc, String.sub inp i (n-i)) in if n = 0 then raise (Scan_error "int") else loop 0 0 ;; (* val charS2 : string -> char * string *) let charS2 = fun inp -> if String.length inp = 0 then raise (Scan_error "char") else (inp.[0], String.sub inp 1 (String.length inp - 1));; (* We re-define nilD and consD to use the new primitive descriptors. We introduce is_nilS to do the pattern-matching on the input, that is, to verify that the input is exhausted. *) let is_nilS x = assert (x = "");; (* val nilDS : string -> unit *) let nilDS = fun x -> is_nilS x; nilA (* val consDS : ('a -> 'b * 'c) -> ('c -> 'd) -> 'a -> 'b * 'd *) let consDS d tD = fun x -> let (vy,l2) = d x in consA vy (tD l2) ;; (* The running example becomes as follows *) let descS2 = consDS intS2 (consDS (litS2 "-th character after ") (consDS charS2 (consDS (litS2 " is ") (consDS charS2 nilDS))));; (* val descS2 : string -> int * (unit * (char * (unit * (char * unit)))) *) descS2 "5-th character after a is f";; (* - : int * (unit * (char * (unit * (char * unit)))) = (5, ((), ('a', ((), ('f', ()))))) *) (* We do further fusion: let the primitive descriptors themselves construct the A-sequence. We re-define the descriptors as follows: *) (* val litS3 : string -> (string -> 'a) -> string -> 'a *) let litS3 str = fun tD inp -> if String.length str <= String.length inp && str = String.sub inp 0 (String.length str) then tD (String.sub inp (String.length str) (String.length inp - String.length str)) else raise (Scan_error "lit") ;; (* val intS3 : (string -> 'a) -> string -> int * 'a *) let intS3 = fun tD inp -> let n = String.length inp in let rec loop acc i = if i >= n then consA acc (tD "") else let c = inp.[i] in if c >= '0' && c <= '9' then loop (acc * 10 + (int_of_char c - int_of_char '0')) (succ i) else if i = 0 then raise (Scan_error "int") else consA acc (tD (String.sub inp i (n-i))) in if n = 0 then raise (Scan_error "int") else loop 0 0 ;; (* val charS3 : (string -> 'a) -> string -> char * 'a *) let charS3 = fun tD inp -> if String.length inp = 0 then raise (Scan_error "char") else consA inp.[0] (tD (String.sub inp 1 (String.length inp - 1)));; (* We re-define consD to use the new primitive descriptors. *) (* val consDS : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c *) let consDS d tD = fun x -> d tD x ;; (* The inferred type indicates this is essentially the identity function; it becomes one after two eta-reductions *) (* The running example becomes as follows *) (* Since consD is the identity, we could have just dropped all consD below *) let descS3 = consDS intS3 (consDS (litS3 "-th character after ") (consDS charS3 (consDS (litS3 " is ") (consDS charS3 nilDS))));; (* val descS3 : string -> int * (char * (char * unit)) *) descS3 "5-th character after a is f";; (* - : int * (char * (char * unit)) = (5, ('a', ('f', ()))) *) (* We have indeed got rid of the dummies *) (* We'd like to have a different representation for argument list, as a sequence of arguments, so to speak, rather than a nested tuple. We choose a different representation for consA and nilA. *) let nilA = fun f -> f;; let consA h t = fun f -> t (f h);; (* The intuition for this choice: (consA x1 (consA x2 nilA)) is (beta-)equivalent to fun f -> f x1 x2 So, we represent the collection as a sequence of arguments applied to some function. *) (* Now, we re-define everything to use the new definition of 3 as being nested tuples, we can use consD and nilD. However, we use the new definitions of nilA and consA: *) (* val nilDS : string -> 'a -> 'a *) let nilDS = fun x -> is_nilS x; nilA (* val litS4 : string -> (string -> 'a) -> string -> 'a *) let litS4 str = fun tD inp -> if String.length str <= String.length inp && str = String.sub inp 0 (String.length str) then tD (String.sub inp (String.length str) (String.length inp - String.length str)) else raise (Scan_error "lit") ;; (* val intS4 : (string -> 'a -> 'b) -> string -> (int -> 'a) -> 'b *) let intS4 = fun tD inp -> let n = String.length inp in let rec loop acc i = if i >= n then consA acc (tD "") else let c = inp.[i] in if c >= '0' && c <= '9' then loop (acc * 10 + (int_of_char c - int_of_char '0')) (succ i) else if i = 0 then raise (Scan_error "int") else consA acc (tD (String.sub inp i (n-i))) in if n = 0 then raise (Scan_error "int") else loop 0 0 ;; (* val charS4 : (string -> 'a -> 'b) -> string -> (char -> 'a) -> 'b *) let charS4 = fun tD inp -> if String.length inp = 0 then raise (Scan_error "char") else consA inp.[0] (tD (String.sub inp 1 (String.length inp - 1)));; (* The running example becomes as follows *) (* Since consD is the identity, we could have just dropped all consD below *) let descS4 = consDS intS4 (consDS (litS4 "-th character after ") (consDS charS4 (consDS (litS4 " is ") (consDS charS4 nilDS))));; (* val descS4 : string -> (int -> char -> char -> '_a) -> '_a *) (* Thus, we have derived scanf with the consumer function! *) descS4 "5-th character after a is f" (fun x1 x2 x3 -> (x1,x2,x3));; (* - : int * char * char = (5, 'a', 'f') *) (* Thus we have derived sscanf of OCaml! *) (* Examination of the inferred types of litS4, charS4, etc. demonstrates the relationship with CPS. Indeed, if we drop consD, we notice that a format descriptor is a functional _composition_ of the primitive descriptors, applied to nilDS. *) (* We can re-define the descriptors so that the input comes from a file descriptor rather than from a string. We can easily abstract over primitive descriptors: However, since they are polymorphic functions (which is obvious from their type), we need first-class polymorphism. Fortunately, OCaml supports such polymorphism in the form of polymorphic records. *) (* The type 'i is the type of the input to scanf: a string, a buffer, a file descriptor, etc. *) type 'i descriptorS = { litS : 'w. string -> ('i -> 'w) -> 'i -> 'w; intS : 'a 'w. ('i -> 'a -> 'w) -> 'i -> (int -> 'a) -> 'w; charS : 'a 'w. ('i -> 'a -> 'w) -> 'i -> (char -> 'a) -> 'w; };; (* primitive descriptors, this time generic *) let litS5 str = fun {litS = f} -> f str;; let intS5 = fun x -> x.intS;; let charS5 = fun x -> x.charS;; (* General composition function: composing two descriptors *) (*val ( ^^ ) : ('a -> 'b -> 'c) -> ('a -> 'd -> 'b) -> 'a -> 'd -> 'c *) let (^^) f g = fun d -> fun x -> f d (g d x);; (* val descS5 : '_a descriptorS -> ('_a -> '_b -> '_c) -> '_a -> (int -> char -> char -> '_b) -> '_c *) let descS5 = intS5 ^^ litS5 "-th character after " ^^ charS5 ^^ litS5 " is " ^^ charS5;; (* The following function implements the interface of OCaml's sscanf, taking the input from a string *) (* val scanf4 : (string descriptorS -> (string -> 'a -> 'a) -> 'b -> 'c) -> 'b -> 'c = *) let scanf4 d inp = d {litS=litS4;intS=intS4;charS=charS4} nilDS inp;; scanf4 descS5 "5-th character after a is f" (fun x1 x2 x3 -> (x1,x2,x3));; (* - : int * char * char = (5, 'a', 'f') *) (* We have derived the real sscanf. We can derive a scanf that takes input from a file, using the same file descriptor. *) (* ======================================================================== Deriving printf *) (* We rely on the symmetry between printf and scanf: nilA <-> nilS and consA <-> consS To illustrate symmetry, we look back at descS2 version of scanf (with tuples for the A-sequence and string for the O-sequence). The dummy argument is still present. We `invert' the definition (by interchanging the A and O-sequences) to obtain a version of printf. *) let nilS = "" let consS = (^);; (* val is_nilA : unit -> unit *) let is_nilA x = assert (x = ());; (* val nilDP : unit -> string *) let nilDP = fun x -> is_nilA x; nilS;; (* val consDP : ('a -> string * 'b) -> ('b -> string) -> 'a -> string *) let consDP d tD = fun x -> let (vy,l2) = d x in consS vy (tD l2) ;; (* The primitive printf descriptors, the dual of scanf descriptors litS2, intS2, charS2. They de-construct the A-sequence on their own. *) (* val litP2 : string -> unit * 'a -> string * 'a *) let litP2 str = fun arg -> let ((),l2) = arg in (litP str (),l2) ;; (* val intP2 : int * 'a -> string * 'a *) let intP2 = fun arg -> let (n,l2) = arg in (intP n,l2) ;; (* val charP2 : char * 'a -> string * 'a *) let charP2 = fun arg -> let (c,l2) = arg in (charP c,l2) ;; (* The running example becomes as follows *) (* It is descS2 with S replaced by P *) let descP2 = consDP intP2 (consDP (litP2 "-th character after ") (consDP charP2 (consDP (litP2 " is ") (consDP charP2 nilDP))));; (* val descP2 : int * (unit * (char * (unit * (char * unit)))) -> string For comparison, descS2 had the type val descS2 : string -> int * (unit * (char * (unit * (char * unit)))) *) descP2 (5,((),('a',((),('f',())))));; (* - : string = "5-th character after a is f" *) (* Getting rid of the dummy unit arguments is significantly simpler now. We merely modify litP2 (* val litP3 : string -> 'a -> string * 'a *) let litP21 str = fun arg -> let l2 = arg in (litP str (),l2) ;; let intP21 = intP2;; let charP21 = charP2;; OTH, we just proceed as we did with scanf *) (* We do further fusion: let the primitive descriptors themselves construct the S-sequence. We re-define the descriptors as follows: *) (* val litP3 : string -> ('a -> string) -> 'a -> string *) (* val litS3 : string -> (string -> 'a) -> string -> 'a *) let litP3 str = fun tD arg -> consS str (tD arg) ;; (* val intS3 : (string -> 'a) -> string -> int * 'a *) (* val intP3 : ('a -> string) -> int * 'a -> string *) let intP3 = fun tD arg -> let (n,l2) = arg in consS (intP n) (tD l2) ;; (* val charP3 : ('a -> string) -> char * 'a -> string *) let charP3 = fun tD arg -> let (c,l2) = arg in consS (charP c) (tD l2) ;; (* Since consDS that uses primitive descriptors litS3, etc. no longer depends on consS or consA, consDP is the same as consDS: just the identity function *) (* val consDS : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c *) let consDP d tD = fun x -> d tD x ;; let descP3 = consDP intP3 (consDP (litP3 "-th character after ") (consDP charP3 (consDP (litP3 " is ") (consDP charP3 nilDP))));; (* val descP3 : int * (char * (char * unit)) -> string = *) descP3 (5,('a',('f',())));; (* - : string = "5-th character after a is f" *) (* We now turn to the representation for the A-sequence that we used for scanf. Recall: let nilA = fun f -> f;; let consA h t = fun f -> t (f h);; How to write un_consA? it should satisfy the equation un_consA (consA h t) = (h,t) Since (consA h t) is a function, the only thing we can do is to apply it. (consA h t) (fun h -> ...) = (h,t) or, after inlining consA: t ((fun h -> ...) h) -> (h,t) Now we see the problem: ... needs to obtain one of its arguments, t, from its context! The solution is known: we can either use CPS or direct-style (that is, delimited control). The derivation in CPS style is given in derive6.ml, which yields printfC4 through printfC7. The following is the direct-style derivation using multi-prompt delimited control. The parallel direct-style derivation for the ordinary shift/reset is given in Derive5.hs. *) open Delimcc;; (* val un_consA : (('a -> 'b) -> 'c) -> 'a * ('b -> 'c) *) let un_consA x = let pr = new_prompt () in let p = new_prompt () in push_prompt pr (fun () -> push_prompt p (fun () -> x (fun h -> shift p (fun t -> abort pr (h,t)))); failwith "unreachable") ;; let is_nilA x = x () = ();; (* Contrast the inferred type of un_consA with that of consA: val un_consA : (('a -> 'b) -> 'c) -> 'a * ('b -> 'c) The inverse is easy to see. *) (* The example of the de-construction: *) let l1 = consA 5 (consA 'a' (consA 'f' nilA));; let (h1,l11) = un_consA l1;; (* val h1 : int = 5 val l11 : (char -> char -> '_a) -> '_a = *) let (h2,l12) = un_consA l11;; (* val h2 : char = 'a' val l12 : (char -> '_a) -> '_a = *) let (h3,l13) = un_consA l12;; (* val h3 : char = 'f' val l13 : '_a -> '_a = *) (* val litP4 : string -> ('a -> string) -> 'a -> string *) let litP4 = litP3;; (* val intP4 : (('a -> 'b) -> string) -> ((int -> 'a) -> 'b) -> string *) let intP4 = fun tD arg -> let (n,l2) = un_consA arg in consS (intP n) (tD l2) ;; let charP4 = fun tD arg -> let (c,l2) = un_consA arg in consS (charP c) (tD l2) ;; (* is_nilA x should ``test'' if x is the identity function. The following is just as good test as applying x to () and checking that the result is unit. *) let nilDP = fun x -> x nilS;; let descP4 = consDP intP4 (consDP (litP4 "-th character after ") (consDP charP4 (consDP (litP4 " is ") (consDP charP4 nilDP))));; (* val descP4 : ((int -> char -> char -> string) -> string) -> string = *) descP4 (consA 5 (consA 'a' (consA 'f' nilA)));; (* - : string = "5-th character after a is f" *) (* We would like the argument sequence to be just the application of the arguments. We would like to transform (xxx x1 x2 x3) to (fun f -> f x1 x2 x3), which is needed as an argument to descP4. We can do that if we enclose the whole printf expression in reset. *) let p = new_prompt () in push_prompt p (fun () -> shift p descP4 5 'a' 'f') ;; (* - : string = "5-th character after a is f" () *) (* Since printf always returns the string, the prompt p can be made global. Alas, the need to enclose the whole expression into reset is unsatisfying. *) (* Begin experiment: using control instead of shift. The derivation in the paper indicates that control should work just as well, if not better (no extra reset inserted). In the following, we repeat the development leading to descP4 using control instead of shift. Everything works as before. *) let un_consA x = let pr = new_prompt () in let p = new_prompt () in push_prompt pr (fun () -> push_prompt p (fun () -> x (fun h -> control p (fun t -> abort pr (h,t)))); failwith "unreachable") ;; let is_nilA x = x () = ();; (* Contrast the inferred type of un_consA with that of consA: val un_consA : (('a -> 'b) -> 'c) -> 'a * ('b -> 'c) The inverse is easy to see. *) (* The example of the de-construction: *) let l1 = consA 5 (consA 'a' (consA 'f' nilA));; let (h1,l11) = un_consA l1;; (* val h1 : int = 5 val l11 : (char -> char -> '_a) -> '_a = *) let (h2,l12) = un_consA l11;; (* val h2 : char = 'a' val l12 : (char -> '_a) -> '_a = *) let (h3,l13) = un_consA l12;; (* val h3 : char = 'f' val l13 : '_a -> '_a = *) (* val litP4 : string -> ('a -> string) -> 'a -> string *) let litP4 = litP3;; (* val intP4 : (('a -> 'b) -> string) -> ((int -> 'a) -> 'b) -> string *) let intP4 = fun tD arg -> let (n,l2) = un_consA arg in consS (intP n) (tD l2) ;; let charP4 = fun tD arg -> let (c,l2) = un_consA arg in consS (charP c) (tD l2) ;; (* is_nilA x should ``test'' if x is the identity function. The following is just as good test as applying x to () and checking that the result is unit. *) let nilDP = fun x -> x nilS;; let descP4 = consDP intP4 (consDP (litP4 "-th character after ") (consDP charP4 (consDP (litP4 " is ") (consDP charP4 nilDP))));; (* val descP4 : ((int -> char -> char -> string) -> string) -> string = *) descP4 (consA 5 (consA 'a' (consA 'f' nilA)));; (* - : string = "5-th character after a is f" *) let p = new_prompt () in push_prompt p (fun () -> control p descP4 5 'a' 'f') ;; (* - : string = "5-th character after a is f" () *) (* End of experiment: using control instead of shift *) (* A different take, viewing un_consA as `reading' from a sequence. *) (* The following is a ad hoc, especially the introduction of the accumulator *) (* The accumulation is clearly very inefficient; that could be fixed; e.g., by making the accumulator a list of strings, to be accumulated in the reverse order and concatenated at the very end. *) let un_consA1 p = shift p (fun k h -> let p' = new_prompt () in push_prompt p' (fun () -> k (h,p'); failwith "out-of-bound ret")) ;; (* Again, it works if shift replaced with control, or even control0 (aka, cupto). To test this, uncomment the definition below. let un_consA1 p = control p (fun k h -> let p' = new_prompt () in push_prompt p' (fun () -> k (h,p'); failwith "out-of-bound ret")) ;; *) let nilDP5 = fun p acc -> abort p acc;; (* litP5 : string -> ('a -> string -> 'b) -> 'a -> string -> 'b *) let litP5 str = fun tD arg acc -> (tD arg) (consS acc str) ;; (* The only difference from intP4/charP4 is the introduction of the accumulator and the use of un_consA1 rather than un_consA. *) (* val intP5 : ('a Delimcc.prompt -> string -> 'b) -> (int -> 'a) Delimcc.prompt -> string -> 'b = *) let intP5 = fun tD arg acc -> let (n,l2) = un_consA1 arg in (tD l2) (consS acc (intP n)) ;; let charP5 = fun tD arg acc -> let (c,l2) = un_consA1 arg in (tD l2) (consS acc (charP c)) ;; (* printf5 : ('a Delimcc.prompt -> string -> 'a) -> 'a *) let printf5 desc = let p = new_prompt () in push_prompt p (fun () -> desc p "");; (* Note, consDP remains as before: the identity function *) let descP5 = consDP intP5 (consDP (litP5 "-th character after ") (consDP charP5 (consDP (litP5 " is ") (consDP charP5 nilDP5))));; (* val descP5 : (int -> char -> char -> string) Delimcc.prompt -> string -> 'a *) printf5 descP5;; (* - : int -> char -> char -> string = *) printf5 descP5 5 'a' 'f';; (* - : string = "5-th character after a is f" *)