Apologies if I miss something fundamental, but having used Jane Street’s libraries I just can’t understand why t-first is not the default convention.
It makes the type inference actually work and defining APIs is more straightforward:
- start with the base type and then add everything else
what are the advantages of t-last?
For iterators like
t last allows to write code using
|> like this:
[ 1; 2; 3 ]
|> List.map ((+) 1)
|> List.map string_of_int
|> List.iter print_endline
On the other hand, for those iterators, having
t first indeed usually helps with record (and sum type) disambiguation, and allows to use the following style:
List.iter [ 1; 2; 3 ] @@ fun i ->
print_endline (string_of_int (i + 1))
which I personally like a lot (I usually define
let list_iter x f = List.iter f x and the same for other iterators).
t last often makes sense as the only non-labeled argument of the function, replacing
unit. For instance:
type ('k, 'v) store
val store: key: 'k -> value: 'v -> ?must_be_unique: bool -> ('k, 'v) store -> ('k, 'v) store
val store: key: 'k -> value: 'v -> ?must_be_unique: bool -> store: ('k, 'v) store -> unit -> ('k, 'v) store
So really both have their pros and cons. What is important is consistency.
(I wish the stdlib had
List.map' etc. for the reverse order though )
Also note that there is often a middle ground which has the better composability of t-last and the stronger disambiguation of t-first. Consider the type of e.g.
val fold : (key -> 'val -> 'acc -> 'acc) -> 'val t -> 'acc -> 'acc
If you label the function argument, thereby enabling reordering it at call sites, then you can reorder it in the arguments of the signature to give stronger type-based disambiguation:
val fold : 'val t -> 'acc -> f:(key -> 'val -> 'acc -> 'acc) -> 'acc
In this way, the arguments of
f benefit from the types of the map and accumulator passed at the call site (no matter their order relative to
~f), and you can still partially-apply
fold to obtain an “accumulator transformer” of type
'acc -> 'acc.
The benefits of enabling partial applications to yield “accumulator transformers” of type
'acc -> 'acc are only fully seen when the rest of the APIs are arranged so that functions returning a “t” accept the argument “t” last, so that the functions like
Map.S.add can be partially applied to yield something of type
'acc -> 'acc. Such structural types are very general and very composable, and with libraries set up that way, it is very easy to use types such as
'val t as accumulators or states in functional “state-passing style” code. The importance of this sort of thing varies depending on the sort of code you’re writing.
I tend to use the
|> style quite a bit so t-last is nice. For my own APIs, the location of
t tends to have semantic meaning: t-first means the function modifies the value in-place. t-last means it’s immutable.
I believe that the original motivation for this convention (which if you look carefully in the
stdlib is only applied to immutable data structures) was to facilitate currying. For example,
Set.add x has signature
Set.t -> Set.t which seems more useful than the alternative, in which the resulting function
Set.add set would have signature
Set.elt -> Set.t.
Okay thank you all, I think I have a better understanding
I guess the type of code I am writing in OCaml is very similar to what I’m used to with other languages - limited currying and chaining, so t-first and labels fits right in
I’ve been looking at what “conventional” OCaml code should look like, but couldn’t find resources
In F# there are some (I found helpful) guidelines for code style, naming conventions, component design and etc. There’s even a section on point-free-programming and why it is discouraged in F# https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions#partial-application-and-point-free-programming ; I mean it is very MS to set guidelines and etc. but at the same time it’s nice to see somewhat consistent code when checking third party libraries source and etc., and also as a newcomer it gives you some idea of what to aim for
nevermind, I am going in the wrong direction, I understand now that there are pros depending on the code style: create functions -> pass data as opposed to: take data -> pass it to functions