Why isn't optional use of labels allowed with every argument?

I wonder why isn’t the optional use of labels allowed with every argument and why must I explicitly mark parameters where I must use labelled arguments with tilde? What’s the reason for this design decision?

I don’t know if there are technical reasons, but allowing the same argument to be supplied both positionally and named sounds like an awful idea to me that will make it really hard for others to read your code. More freedom is not always better.

But I may be wrong. May I ask what use case you have in mind?

The first issue is that of allowing labelled arguments to be accessed positionally, as in Ruby and Python. I believe mostly it’s about the problem of changing labelled argument order. You wouldn’t expect that changing the order of labelled arguments in a function would have issues, but indeed it would if you allow labelled arguments to be accessed both by position and label. In the current scheme, labelled arguments can be moved around, while positional arguments must maintain their position.

Additionally, there’s a separate question of allowing a particular argument to be accessed using a label. If you want to allow any argument to do so (rather than marking specific ones), you must export the name of every argument in the type system. You’d never be allowed to have a function that only takes an int for example – it must export the label. Given the first design decision (not allowing to access labelled arguments positionally) this would make functions very brittle, as you’d have to get every label exactly right everywhere.

I don’t have any particular use case in mind.

Just asking why should I decide ahead of time which arguments will be labelled and which not. Because I don’t see any technical problem when allowing that (only the problem with code readability as you mentioned. But I’m still curious if the readability was the reason to do it how it was done or if there are any technical reasons for it).

You’d never be allowed to have a function that only takes an int for example

But why can’t we allow

let foo a b c d = a + b + c + d in
(* Function taking one argument labelled with d *)
let _ = foo 1 2 3 in
(* Function taking one labelled argument c *)
let _ = foo ~d:4 1 2 in
(* Function taking two labelled arguments b and d *)
let f = foo 1 ~c:3 in
let f = foo ~c:3 1 in
(* Function taking one labelled argument d *)
let _ = f 2 in
(* Function taking one labelled argument b *)
let g = f ~d:4 in
(* Result *)
g ~b:2

Then g can be used as a function taking only int or as a function with 1 labelled argument b of type int.

Automatically generating labels means fun x -> x and fun y -> y have different types. Why must they be different? Consider:

let f x = x
let g y = y

let _ = f ~y:1 (* type error *)

let _ = [ f; g ] (* type error *)

The fact that this change would make fun x -> x and fun y -> y not equivalent speaks to how fundamentally big a change this would be.

Also, you can have constructs more complicated than identifiers on the left-hand side of a lambda, e.g.:

let z = fun (x, y) -> x + y

What should the label for the argument of z be?

However, you can already accomplish something close to what you’re looking for:

let f ~x = x + 1

let _ = f 10    (* 11 *)
let _ = f ~x:10 (* 11 *)

This only works, however, if you fully apply the function and the result type is not a type variable. (see http://caml.inria.fr/pub/docs/manual-ocaml-4.06/lablexamples.html#sec42)

4 Likes

This also works with multiple arguments.

let f a b ~c = (a, b, c)

let _ = f 1 2 3
let _ = f ~c:3 1 2
let _ = f 1 ~c:3 2

all give (1,2,3). So the unlabeled arguments are parsed in order.

After reading the manual, apparently all sets of arguments that have the same label are parsed in order, no label just being a special case. However, I don’t know why someone would want to define a function f ~a ~a.