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