How are ListLabels, ArrayLabels, etc. implemented?

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.

If we look at the ListLabels module implementation on Github, we can see that it includes the List module but that’s it.

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:

Cheers,
Nicolas

1 Like

Any reason why versions with labels are being dropped from new additions? I.e Seq and DynArray stands out.

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…

Cheers,
Nicolas

I use StdLabels a lot, personally.

Edit: they are also used in the js_of_ocaml codebase.

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.

Cheers,
Nicolas

Thanks! Interesting technique, which I can reproduce by compiling manually.

I’d be interested to hear how one could make this work with a dune workflow.

Ideally, something like this would work without any configuration (assume I have the mli files properly written):

(* my_labeled_module.ml *)
[@@@warning "-6"]

include My_normal_module

But it doesn’t. “labels-omitted” warning number 6 seems to be an error.

I tried to turn the error into a warning with a config:

$ tail -n+1 dune-workspace 
(lang dune 3.11)

(context default)

(env
 (dev
  (flags :standard -warn-error -6)))

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.

Cheers,
Nicolas

Sherlocode shows they’re not used that much, see ListLabels and ArrayLabels. I wish they were simply removed from the stdlib…

1 Like

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.

1 Like

Thanks for the feedback Nicolas.

IMO, labeling a parameter when it there is no ambiguity, like f in map, is not very useful.

But it’s pretty nice to have labeled arguments when several arguments have the same type.

# Array.sub;;
- : 'a array -> int -> int -> 'a array = <fun>
# ArrayLabels.sub;;
- : 'a array -> pos:int -> len:int -> 'a array = <fun>

Ideally, Array.sub would be labeled.

4 Likes

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.

1 Like

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 :slight_smile:

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.

4 Likes

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.

3 Likes

One can also write

my_list |> List.iter @@ fun x ->
(* several lines of code here *)

You don’t need labels to move arguments around. :slight_smile:

1 Like

That works for n=2 arguments. How about when you have n > 2 arguments?

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. :slight_smile:

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.

But I claim that there is: labeled arguments.

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.

1 Like