I am thinking of adding index-aware variants of existing functions to the standard library. For instance, List.map has an indexed variant named List.mapi. Currently, only a minority of standard functions for which it would make sense have such a variant (there is no for_alli, existsi, findi…). Besides, which functions have an indexed variant is not entirely consistent across standard modules (fold_lefti exists only in Seq, filteri in List, find_mapi in Seq since 5.1…). This has bitten me many times. Hence I would like to make it systematic.
Affected standard modules: Seq, List, Array, Float.Array, String, Bytes (+ their labeled variants) and I hope I haven’t forgotten any.
Regarding naming: my current idea: because there are many functions involved, and also because it is not always obvious how to follow the existing convention of appending an i to the name (“should that function be called find_opti or findi_opt?”), instead of throwing all the new functions into the main namespace, I am thinking of adding a submodule Indexed to every affected standard module. Then, indexed functions in this submodule would have the same name as their non-indexed counterpart, e.g. List.mapi would be the same thing as List.Indexed.map.
Optional secondary goal: identify a small subset of indexed functions that should consistently be provided in the main namespace, with the i suffix convention. I believe it should include these: iteri, mapi (already exists in all affected modules), fold_lefti (among the most useful of these, and already exists in Seq), to_seqi (has a somewhat different status than the other indexed functions, and already exists in several modules).
This is a relatively easy but large addition to the standard library, so before I rush into code and dump everything at OCaml maintainers, I’d like to gather opinions from the broader community.
- What do you think of adding all these functions systematically?
- What do you think of the naming scheme?
- What should be in the minimal subset mentioned above?
- Implementation: in order to de-duplicate code, should the non-indexed versions be re-implemented as wrappers for the indexed versions, knowing that this would make them slightly slower (one more indirection and a closure allocation)?
- Documentation (the hardest part): how to document all these?
- The most concise and therefore my favorite option (which happens to be the laziest) would be to just write a paragraph at the level of the
Foo.Indexedsubmodule, saying something like “these functions are variants of those found inFoo, which also provide the index as an argument to their callback”. (This style of documentation is found forRandom.State.) - The opposite solution would be to fully document the indexed variants, and reduce the documentation of non-indexed variants to “Like
Indexed.foobut with indexes omitted.” Unfortunately, that would break the flow of documentation in the main namespace of affected modules. - I don’t like duplicate explanations because that’s a waste of the reader’s time/brain, not mentioning having to maintain both in sync.
- The most concise and therefore my favorite option (which happens to be the laziest) would be to just write a paragraph at the level of the
For the record, here is a candidate list of all indexed functions (here with the i suffix convention), according to my scan of current documentation. Commented are those that already exist as of OCaml 5.1.
(* Seq: *)
(*! val iteri : (int -> 'a -> unit) -> 'a t -> unit !*)
(*! val fold_lefti : ('b -> int -> 'a -> 'b) -> 'b -> 'a t -> 'b !*)
val for_alli : (int -> 'a -> bool) -> 'a t -> bool
val existsi : (int -> 'a -> bool) -> 'a t -> bool
val findi : (int -> 'a -> bool) -> 'a t -> 'a option
val findi_index : (int -> 'a -> bool) -> 'a t -> int option (* OCaml 5.1 *)
(*! val find_mapi : (int -> 'a -> 'b option) -> 'a t -> 'b option !*) (* OCaml 5.1 *)
val iter2i : (int -> 'a -> 'b -> unit) -> 'a t -> 'b t -> unit
val fold_left2i : ('a -> int -> 'b -> 'c -> 'a) -> 'a -> 'b t -> 'c t -> 'a
val for_all2i : (int -> 'a -> 'b -> bool) -> 'a t -> 'b t -> bool
val exists2i : (int -> 'a -> 'b -> bool) -> 'a t -> 'b t -> bool
val unfoldi : ('b -> int -> ('a * 'b) option) -> 'b -> 'a t
val iteratei : (int -> 'a -> 'a) -> 'a -> 'a t
(*! val mapi : (int -> 'a -> 'b) -> 'a t -> 'b t !*)
val filteri : (int -> 'a -> bool) -> 'a t -> 'a t
val filter_mapi : (int -> 'a -> 'b option) -> 'a t -> 'b t
val scani : (int -> 'b -> 'a -> 'b) -> 'b -> 'a t -> 'b t
val take_whilei : (int -> 'a -> bool) -> 'a t -> 'a t
val drop_whilei : (int -> 'a -> bool) -> 'a t -> 'a t
val groupi : (int -> 'a -> 'a -> bool) -> 'a t -> 'a t t
val flat_mapi : (int -> 'a -> 'b t) -> 'a t -> 'b t
val concat_mapi : (int -> 'a -> 'b t) -> 'a t -> 'b t
val map2i : (int -> 'a -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
val map_producti : (int -> 'a -> int -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
val partitioni : (int -> 'a -> bool) -> 'a t -> 'a t * 'a t
val partition_mapi : (int -> 'a -> ('b, 'c) Either.t) -> 'a t -> 'b t * 'c t
(* List: *)
(*! val iteri : (int -> 'a -> unit) -> 'a t -> unit !*)
(*! val mapi : (int -> 'a -> 'b) -> 'a t -> 'b t !*)
val rev_mapi : (int -> 'a -> 'b) -> 'a t -> 'b t
val filter_mapi : (int -> 'a -> 'b option) -> 'a t -> 'b t
val flat_mapi : (int -> 'a -> 'b t) -> 'a t -> 'b t
val concat_mapi : (int -> 'a -> 'b t) -> 'a t -> 'b t
val fold_left_mapi : (int -> 'a -> 'b -> 'a * 'c) -> 'a -> 'b t -> 'a * 'c t
val fold_lefti : ('a -> int -> 'b -> 'a) -> 'a -> 'b t -> 'a
val fold_righti : (int -> 'a -> 'b -> 'b) -> 'a t -> 'b -> 'b
val iter2i : (int -> 'a -> 'b -> unit) -> 'a t -> 'b t -> unit
val map2i : (int -> 'a -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
val rev_map2i : (int -> 'a -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
val fold_left2i : ('a -> int -> 'b -> 'c -> 'a) -> 'a -> 'b t -> 'c t -> 'a
val fold_right2i : (int -> 'a -> 'b -> 'c -> 'c) -> 'a t -> 'b t -> 'c -> 'c
val for_alli : (int -> 'a -> bool) -> 'a t -> bool
val existsi : (int -> 'a -> bool) -> 'a t -> bool
val for_all2i : (int -> 'a -> 'b -> bool) -> 'a t -> 'b t -> bool
val exists2i : (int -> 'a -> 'b -> bool) -> 'a t -> 'b t -> bool
val findi : (int -> 'a -> bool) -> 'a t -> 'a
val find_opti : (int -> 'a -> bool) -> 'a t -> 'a option
val findi_index : (int -> 'a -> bool) -> 'a t -> int option (* OCaml 5.1 *)
(*! val find_mapi : (int -> 'a -> 'b option) -> 'a t -> 'b option !*) (* OCaml 5.1 *)
(*! val filteri : (int -> 'a -> bool) -> 'a t -> 'a t !*)
val find_alli : (int -> 'a -> bool) -> 'a t -> 'a t
val partitioni : (int -> 'a -> bool) -> 'a t -> 'a t * 'a t
val partition_mapi : (int -> 'a -> ('b, 'c) Either.t) -> 'a t -> 'b t * 'c t
val to_seqi : 'a t -> (int * 'a) Seq.t
(* Array: *)
val fold_lefti : ('a -> int -> 'b -> 'a) -> 'a -> 'b t -> 'a
val fold_left_mapi : (int -> 'a -> 'b -> 'a * 'c) -> 'a -> 'b t -> 'a * 'c t
val fold_righti : (int -> 'b -> 'a -> 'a) -> 'b t -> 'a -> 'a
val iter2i : (int -> 'a -> 'b -> unit) -> 'a t -> 'b t -> unit
val map2i : (int -> 'a -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
val for_alli : (int -> 'a -> bool) -> 'a t -> bool
val existsi : (int -> 'a -> bool) -> 'a t -> bool
val for_all2i : (int -> 'a -> 'b -> bool) -> 'a t -> 'b t -> bool
val exists2i : (int -> 'a -> 'b -> bool) -> 'a t -> 'b t -> bool
val findi_opt : (int -> 'a -> bool) -> 'a t -> 'a option
val findi_index : (int -> 'a -> bool) -> 'a array -> int option (* OCaml 5.1 *)
val find_mapi : (int -> 'a -> 'b option) -> 'a t -> 'b option
(*! val to_seqi : 'a array -> (int * 'a) Seq.t !*)
(* String: *)
(*! val mapi : (int -> char -> char) -> t -> t !*)
val fold_lefti : ('a -> int -> char -> 'a) -> 'a -> t -> 'a
val fold_righti : (int -> char -> 'a -> 'a) -> t -> 'a -> 'a
val for_alli : (int -> char -> bool) -> t -> bool
val existsi : (int -> char -> bool) -> t -> bool
(*! val iteri : (int -> char -> unit) -> t -> unit !*)
(*! val to_seqi : t -> (int * char) Seq.t !*)
(* Bytes: *)
(*! val iteri : (int -> char -> unit) -> t -> unit !*)
(*! val mapi : (int -> char -> char) -> t -> t !*)
val fold_lefti : ('a -> int -> char -> 'a) -> 'a -> t -> 'a
val fold_righti : (int -> char -> 'a -> 'a) -> t -> 'a -> 'a
val for_alli : (int -> char -> bool) -> t -> bool
val existsi : (int -> char -> bool) -> t -> bool
(*! val to_seqi : t -> (int * char) Seq.t !*)