4

EDIT: I'm sorry everyone, I thought my small examle was complete, turns out it's not. I made a new one that really should be!

As soon as I use a formatter as parameter to Scanf or Printf functions the formatter type gets bound to a in- or output-channel respectively. Is there a way to have a function take a formatter (or string) and use that as a formatter for both printing and reading?

let fmt = format_of_string "%d,%d";;
Scanf.sscanf "2,2" fmt (fun x y -> x,y);;
fmt;;

gives

- : (int -> int -> int * int, Scanf.Scanning.scanbuf, '_a, (int -> int -> int * int) -> int * int, (int -> int -> int * int) -> int * int, int * int) format6 = <abstr>

Which means a subsequent Printf.printf fmt 1 2;; gives a type error. This goes for every combination of format_of_string and Scanf.format_from_string like functions I've tried.

Example:

module Thing = struct

(* Just a helper for file IO *)
type 'a result = Success of 'a | Failure of exn;;
let with_out_file filename fn =
  let out_ch = open_out filename in
  let res = try Success (fn out_ch) with
    exn -> Failure exn in
  close_out out_ch;
match res with
| Success a -> a
| Failure a -> raise a;;

(* Uses the format string for writing *)
let print (fmt : ('a, 'b, 'c, 'd, 'e, 'f) format6) fn v =
  with_out_file fn (fun x -> Printf.fprintf x fmt v);;

(* Uses the format string for reading *)
let read (fmt : ('a, 'b, 'c, 'd, 'e, 'f) format6) v =
  Scanf.sscanf v fmt (fun x -> x);;

(* Where things break *)
let both fmt v =
  read fmt "42\n";
  print fmt "tfile" v;;
end;;

Gives

Error: This expression has type ('a -> 'b, Scanf.Scanning.scanbuf, 'c, ('d -> 'd) -> 'e, ('a -> 'b) -> 'f, 'f) format6 but an expression was expected of type                                                       
     ('a -> 'b, out_channel, unit, unit, unit, unit) format6                                                                                                                                                    
   Type Scanf.Scanning.scanbuf is not compatible with type out_channel

For the last line of the both function, which seems to make sense, but if I remove the both function from the module, I can call read and print with the same format string (same variable as parameter) and it just works.

So, with hopes that you guys haven't given up on me yet; how do I get around that? neither eta-expansion nor type annotation seems to work in this case?

Bladt
  • 303
  • 3
  • 11

3 Answers3

5

You're hitting the infamous value restriction:

# let fmt = format_of_string "%d,%d";;
val fmt : (int -> int -> '_a, '_b, '_c, '_d, '_d, '_a) format6 = <abstr>

Those '_a, '_b, '_c, '_d mean "types to be determined as soon as we can". They are not parameters, i.e., fmt is not a polymorphic value. In contrast, an empty list is polymorphic:

# let emptiness = [] ;;
val emptiness : 'a list = []

Now we have 'a and not '_a. But maybe you already know all this. The point is that when we apply Printf.printf to fmt, its type gets fixed as

(int -> int -> unit, out_channel, unit, unit, unit, unit) format6

but when we apply Scanf.sscanf to fmt its type gets fixed as

(int -> int -> int * int, Scanf.Scanning.in_channel, '_a,
(int -> int -> int * int) -> int * int,
(int -> int -> int * int) -> int * int, int * int)
format6

These two types are not compatible, and because fmt is not polymorphic, you cannot use it both ways. The solution is simply to have two copies, fmt_in and fmt_out. Is there something unacceptable with that solution?

Andrej Bauer
  • 2,458
  • 17
  • 26
  • 'Is there something unacceptable with that solution' -- Sort of. I'm making a module/library, and would like to avoid the uncleanliness of taking the same parameter twice, but also; I've been unable to pull it off. Saving two identical formatters binds them to the same restricted types. Using one binds the type of both. – Bladt Jul 29 '14 at 21:46
  • Explanation: I upvoted your answer (because It is a great answer), but accepted Jeffreys because the question actually states that the problem is with value restriction, and asks for a work-around. – Bladt Jul 29 '14 at 21:53
4

Another idea is to make fmt a function rather than an actual format value:

# let fmt () = format_of_string "%d,%d";;
val fmt : unit -> (int -> int -> 'a, 'b, 'c, 'd, 'd, 'a) format6 = <fun>
# Scanf.sscanf "2,2" (fmt ()) (fun x y -> (x, y));;
- : int * int = (2, 2)
# Printf.printf (fmt ()) 3 4;;
3,4- : unit = ()

This is a little clunky, but maybe not too bad...?

Jeffrey Scofield
  • 65,646
  • 2
  • 72
  • 108
4

Your example does not obey the value restriction since it is a function application. However, the call to format_of_string is not necessary. format_of_string is actually just the identity function with type

('a, 'b, 'c, 'd, 'e, 'f) format6 -> ('a, 'b, 'c, 'd, 'e, 'f) format6

It works by forcing a type annotation on its argument, which causes the literal to be interpreted as a format specifier instead of a string.

Instead you can simply provide the type annotation directly yourself:

# let fmt : ('a, 'b, 'c, 'd, 'e, 'f) format6 = "%d,%d";;
val fmt : (int -> int -> 'f, 'b, 'c, 'e, 'e, 'f) format6 = <abstr>

Note that the type is now fully polymorphic because the expression is now a value.

Update: In OCaml 4.02 you can now shorten the type annotation to:

let fmt : _ format6 = "%d,%d";;
Leo White
  • 1,131
  • 5
  • 9
  • Wonderful! While long type annotations are not exactly pretty, it's far less hacky than wrapping a value in a function. Could you explain _why_ my example is a function application though? Why isn't format_of_string "%d,%d" evaluated to a value? – Bladt Aug 11 '14 at 19:20
  • The value restriction is a *syntactic* restriction. `format_of_string "%d%d"` is syntactically a function application, since function applications can have side-effects it is not considered a value. – Leo White Aug 12 '14 at 09:35
  • But the result stored as fmt, that isn't considered a value either? I would get it if it was a partial application, but it's not. – Bladt Aug 12 '14 at 16:56
  • The result is not important, what matters is whether there may have been side effects whilst creating the result. I suggest you read up on the [value restriction](https://realworldocaml.org/v1/en/html/imperative-programming-1.html#the-value-restriction). – Leo White Aug 13 '14 at 09:26
  • Makes sense, I'm still having some problems with it though. – Bladt Aug 15 '14 at 13:23