When I execute the code
let (a,p) = (2+2, Printf.printf) in p "abc"; p "%d" 3 ;;
I expect to see the output abc3, but get instead
File "f.ml", line 1, characters 46-47:
Error: This function has type (unit, out_channel, unit) format -> unit
It is applied to too many arguments; maybe you forgot a `;'.
The fun part is that if I change 2+2 to 2, it runs.
Why does the code produce an error as is, but not with +2 removed?
OCaml doesn't have a return keyword — the last expression in a function becomes the result of the function automatically.
At its simplest, a variable is an identifier whose meaning is bound to a particular value. In OCaml these bindings are often introduced using the let keyword.
Function in ocaml is a expression. That means, a function is a value that represents the function. This is also called anonymous function, lambda. (* syntax for a function expression *) fun n -> n + 1;; (* applying a function to a value *) (fun n -> n + 1) 2;; (* ⇒ 3 *)
A combination of OCaml's special typing trick for printf and value polymorphism.
As you may probably know, Printf.printf does not take string but data type format. OCaml type checker has a special rule for typing of string literals for printf: if it is typed as format if the context requests:
# "%d";;
- : string = "%d"
# ("%d" : _format);;
- : (int -> 'a, 'b, 'a) format = ...
OCaml type system has another trick called value polymorphism (more precisely, it is relaxed value polymorphism). Its purpose is to type expressions with side effects correctly. I do not explain its details but it restricts polymorphism: some forms of expressions called "expansive" cannot have polymorphic types:
# fun x -> x;;
- : 'a -> 'a = <fun>
# (fun x -> x) (fun x -> x)
- : '_a -> '_a = <fun>
In the above, (fun x -> x) (fun x -> x) has no polymorphic type, while the identity function fun x -> x has. This is due to the shape of the expression of (fun x -> x) (fun x -> x): it is "expansive". The strange type variable '_a is monomorphic type variable: it can be instantiated to some type only once. On the other hand, polymorphic variables like 'a can be instantiated to a different type for each use of the vlaue.
Let's go back to your code:
# let (a, p) = (2, Printf.printf);;
val a : int
val p : ('a, out_channel, unit) format -> 'a
Here, p has a polymorphic type ('a, out_channel, unit) format -> 'a. 'a can be instantiated to more than one types therefore p "abc"; p "%d" 3 is typable: the polymorphic type can be instantiated to (unit, out_channel, unit) format -> unit for the first use of p, and (int -> unit, out_channel, unit) format -> int -> unit for the second use of p.
Once you change the constant 2 to 2+2, which is expansive, the entire expression becomes expansive too, and the typing changes:
# let (a, p) = (2+2, Printf.printf);;
val a : int
val p : ('_a, out_channel, unit) format -> '_a
Here, p has no longer polymorphic variables 'a but monomorphic '_a. This monomorphic variable is unified (instantiated) to unit at the first use of p, and as a result p's type becomes (unit, out_channel, unit) format -> unit. It can take only 1 argument therefore the typing of the second use of p with 2 arguments fails.
One easy way to avoid this situation is to split your definition into two:
let a = 2 + 2 in
let p = Printf.printf in
p "abc"; p "%d" 3
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With