How are the “labled” modules of the standard library implemented?
If I follow the definition of, let’s say, ListLabels.map, vscode points me to the implementation of List.map, ArrayLabels.map points to Array.map and so on.
We do see a signature file specific to ListLabels though.
I looked around but can’t find anything obvious at the moment.
I also tried to replicate this setup with a module of my own, but it’s a no go: the signature has to match the implementation. To be clearer, I can’t include a non-labeled module, to specify labeled arguments only via an mli file.
The implementation of labeled modules (which are, as you noticed, unlabeled) are compiled with -nolabels which tells the typechecker to ignore labels altogether. In particular this makes the (unlabeled) implementations compatible with the (labeled) interfaces.
The -nolabels flag is passed via the Compflags script:
I don’t know. Maybe there hasn’t been enough demand for them? My sense is that the existing labeled modules of the standard library are not used very widely, but I don’t have any factual information either way…
Yes, they are used in some projects (notably Dune and JSOO), but I don’t have any hard numbers about how much they are used in the large, besides some anecdotical evidence. This can probably be obtained by grepping over the OPAM sources and could be useful information to have to inform the upstream developers.
But that doesn’t work. I can turn other errors into warnings with this dune specific technique, but not this one. It seems that labels-omitted is an error now, not a warning.
I think it will be hard to do because as far as I know it is not possible to have Dune pass a separate set of flags when compiling the interface and the implementation of a given module.
I once tried to suggest this but I gave up. I still think they mostly add more noise to the system but they seem cherished by enough people so that this is unlikely to happen.
Actually it is, because it allows you to supply the parameters in either order, which is very useful for pipin g
with |> and also means you don’t have to remember the order.
What I meant is that map already has its arguments in an order which is pipe-friendly (unlike some other functions I bump into every now and then).
There is no ambiguity and it seems to me that the labeled version is verbose for no obvious benefit.
# [1;2;3] |> List.map (( * ) 2);;
- : int list = [2; 4; 6]
# [1;2;3] |> ListLabels.map ~f:(( * ) 2);;
- : int list = [2; 4; 6]
Also, you couldn’t ever reverse the labeled argument of map with piping (+ it’d be such an odd use-case IMO)
# ~f:(( * ) 2) |> ListLabels.map [1;2;3];;
Error: Syntax error
# (( * ) 2) |> ListLabels.map [1;2;3];;
Warning 6 [labels-omitted]: label f was omitted in the application of this function.
- : int list = [2; 4; 6]
But it’s just something I noticed, I don’t feel too strongly about it
Not so relevant for maps, but for folds note that using a signature such as:
val fold : 'val t -> 'acc -> f:(key -> 'val -> 'acc -> 'acc) -> 'acc
yields stronger type-based disambiguation power for the the definition of f if it is written as a lambda, as compared to:
val fold : (key -> 'val -> 'acc -> 'acc) -> 'val t -> 'acc -> 'acc
So labeling f and placing it as the last argument gives stronger disambiguation power from the type-checker and allows reordering the arguments at call sites to either enable piping, or to put the lambda last which is generally more readable in cases where it is long compared to the other arguments.
Related questions come up not and again, see e.g. here.
Not a huge fan of ~f either but the argument for it in relation to piping is more that you need to put the collection last because of piping, but when not piping it is often clearer if the function is last, which the label lets you do. The point is to write something like this:
List.iter my_list ~f:(fun x ->
(* several lines of code here *)
);
so that you don’t end up with the collection argument being very far from the function name or with having to name the lambda argument. It also makes your code look a bit like a for loop, which is neat.
Not sure about n > 2 arguments. You can always move the last argument with |>, and in practice that seems to cover a lot of cases due to how List arranges the arguments. I’m not claiming that there’s a universal way to move arbitrary arguments around.
Yes, I don’t use the *Labels modules so I sometimes do this. But I find using the piping operator in this way slightly unpleasant, something a bit obfuscating to it I guess, and also it doesn’t work for folds because the initial state argument comes after the function. Anyway all this is very subjective, at the end of the day I think many people consider writing the arguments in the right order without any tricks is completely fine.
That is part of why labeled arguments are so compelling: they solve the problem of argument ordering in a universal, uniform way. One less thing to think about when you design functions or apply them. Why waste brain power on coming up with a different ad-hoc solution each time?
Uniform conventions are a powerful thing, especially in large industrial code bases.