When can you call functions with labeled arguments without the labels?

Viewed 384

It seems that in certain situations you can call functions with labeled arguments without the labels if they're in the right order; e.g.

let f ~x ~y = Format.sprintf "%d %s" x y;;
f 3 "test";;

runs successfully, but

f "test" 3;;

fails with the error message

Line 1, characters 2-8:
Error: This expression has type string but an expression was expected of type
         int

For functions with optional arguments, it seems to work if you don't pass in the optional arguments:

let f ?(x = 1) ~y () = Format.sprintf "%d %s" x y;;
f "help" ();;

succeeds, but

f 2 "help" ();;

fails with the error message

Line 1, characters 4-10:
Error: The function applied to this argument has type
         ?x:int -> y:string -> string
This argument cannot be applied without label

Is there a general rule for when this is possible?

2 Answers

You can omit labels if the application is total (i.e., all required arguments are provided) and if the return type of the function is not a type variable. Arguments to the optional parameters must always be passed by a label.

Let's do some examples,

let example1 ?(opt=0) ~a ~b ~c unlabeled = 
  opt + a + b + c + unlabeled;;

example1 1 2 3 4;;
- : int = 10

Here we were able to apply all arguments without labels, because we have provided all required arguments (the optional parameter is not required, hence the name), and the resulting type is not polymorphic. However, if we will take the List.fold function from Core or ListLabels, which has type

'a list -> init:'accum -> f:('accum -> 'a -> 'accum) -> 'accum

Then we will get,

List.fold [1;2;3;4] 0 (+);;
- : init:(int -> (int -> int -> int) -> '_weak1) ->
    f:((int -> (int -> int -> int) -> '_weak1) ->
       int -> int -> (int -> int -> int) -> '_weak1) ->
    '_weak1

instead of 10, which one would expect. The reason for that is since the resulting type 'accum is a type variable, so it could also be a function, e.g., int -> int or string -> int -> unit, etc -- all those types match with the 'accum type. That basically means that this function accepts a potentially infinite number of positional arguments. Therefore, all arguments that we provided were interpreted as positional, and as a result, we never were able to fill in the labeled arguments, therefore, our application wasn't made total. This actually makes our original definition of the rule at the beginning of the posting redundant - since an application, which type is denoted with a type variable, could never be total.

Note, that this problem only occurs when the return type is a type variable, not just includes some type variables, for example, we can easily omit labels with the List.map function, e.g, given List.map from Core, which has type

'a list -> f:('a -> 'b) -> 'b list

we can easily apply it

# List.map [1;2;3] ident;;
- : int Core_kernel.List.t = [1; 2; 3]

With all that being said, it is usually assumed a bad practice to omit the labels. Mainly because of the caveats with polymorphic return types and because it makes the order of the labeled arguments important, which is sort of counter-intuitive.

Yes, there is a general rule for this. From the manual:

Formal parameters and arguments are matched according to their respective labels, the absence of label being interpreted as the empty label. This allows commuting arguments in applications. One can also partially apply a function on any argument, creating a new function of the remaining parameters.

If several arguments of a function bear the same label (or no label), they will not commute among themselves, and order matters. But they can still commute with other arguments.

As an exception to the above parameter matching rules, if an application is total (omitting all optional arguments), labels may be omitted. In practice, many applications are total, so that labels can often be omitted.

Related