chenglou
chenglou

Reputation: 3640

OCaml labelled arguments order with higher-order functions

In this section of Real World OCaml, it basically says:

let apply_to_tuple f (first, second) = f ~first ~second
let apply_to_tuple_2 f (first, second) = f ~second ~first
let divide ~first ~second = first / second

This allows apply_to_tuple divide (3, 4) to work, but not apply_to_tuple_2 divide (3, 4). The latter throws:

Error: This expression has type first:int -> second:int -> int
   but an expression was expected of type second:'a -> first:'b -> 'c

I was wondering why this was the case. It seems that there isn't any ambiguity here and the compiler could have inferred everything correctly?

Upvotes: 2

Views: 286

Answers (2)

Leo White
Leo White

Reputation: 1151

Whilst it may seem that

first:int -> second:int -> int

and

second:int -> first:int -> int

are the same, they actually aren't due to side-effects.

Consider the following two functions:

let foo ~a =
  print_endline "a";
  fun ~b ->
    print_endline "b"

let bar ~b =
  print_endline "b";
  fun ~a ->
    print_endline "a"

foo has type a:'a -> b:'a -> unit whilst bar has type b:'a -> a:'a -> unit. foo prints "a" after it takes its first argument and "b" after it takes its second argument. bar prints "b" after it receives its first argument and "a" after it receives its second argument.

OCaml ensures that the side-effects in these functions occur exactly when all the arguments before that side-effect have been provided.

So foo will print "a" once it has been given an argument labelled ~a:

# let foo2 = foo ~a:();;
a
val foo2 : b:'_a -> unit = <fun>

and it will print "b" once both ~a and ~b have been provided:

# foo2 ~b:();;
b
- : unit = ()

Whereas bar won't print anything when given an argument labelled ~a:

# let bar2 = bar ~a:();;
val bar2 : b:'a -> unit = <fun>

since both print statements are underneath the ~b argument. Once it is also given the ~b argument -- so that both ~a and ~b have been provided -- it will print both "b" and "a":

# bar2 ~b:();;
b
a
- : unit = ()

Maintaining the correct ordering of side-effects like this requires OCaml to treat the first labelled argument differently from the second labelled argument:

  • When OCaml sees an application of the first labelled argument it must apply the function and perform any side-effects underneath it.

  • When OCaml sees an application of the second argument it instead must build a new function which is waiting for the first argument, and when it receives it will apply the original function using both the first and second arguments.

This means that the order of arguments in the type is significant, and you cannot simply use a second:int -> first:int -> int value where a first:int -> second:int -> int value was expected.

It's possible that OCaml could try to differentiate applications of the first argument from other applications at run-time, and so not need to keep track of this in the type system, but that would make labelled functions much less efficient than regular functions.

Upvotes: 4

Jeffrey Scofield
Jeffrey Scofield

Reputation: 66818

OCaml lets you call omit the parameter names, as long as you supply all the parameters. In such a case, the parameters are taken in order. For this reason, the types first:'a -> second:'b -> 'c and second:'b -> first:'a -> 'c are different.

It seems to me that you would need to give up the ability to call without names in order to get the flexibility you want.

# let f ~a ~b = a - b;;
val f : a:int -> b:int -> int = <fun>
# f 4 3;;
- : int = 1

You can specify a particular order for the f parameter of apply_to_tuple2, which makes the typing work.

# let apply_to_tuple2 (f: first:'a -> second:'b -> 'c) (first, second) =
    f ~second ~first;;
val apply_to_tuple2 : (first:'a -> second:'b -> 'c) -> 'a * 'b -> 'c = <fun>
# let divide ~first ~second = first / second;;
val divide : first:int -> second:int -> int = <fun>
# apply_to_tuple2 divide (3, 4);;
- : int = 0

Update

Here's are a few more details about what I'm claiming.

First, the types of apply_to_tuple2 and divide:

# let apply_to_tuple_2 f (first, second) = f ~second ~first;;
val apply_to_tuple_2 : (second:'a -> first:'b -> 'c) -> 'b * 'a -> 'c = <fun>
# let divide ~first ~second = first / second;;
val divide : first:int -> second:int -> int = <fun>

So, the type of the f parameter of apply_to_tuple2 is second:'a -> first:'b -> 'c. But the type of divide is first:int -> second:int -> int. These types can't be unified, because the order of named parameters matters in OCaml.

If you changed OCaml so that the order of named parameters doesn't matter, you could make these types match. But that's not how OCaml works right now.

Furthermore, if you did make this change you would have to also drop the feature of OCaml whereby you can omit parameter names in some cases. Hence, it would be an incompatible change to the language.

Upvotes: 2

Related Questions