insertWith :: Ord k => (a -> a -> a) -> k -> a -> Map k a -> Map k a
adjust :: Ord k => (a -> a) -> k -> Map k a -> Map k a
findWithDefault :: Ord k => a -> k -> Map k a -> a
The other-key-value-datatype argument order predates containers, it was yanked directly from DData (commit) and all I can find from that era are mailing lists, which are a pain to traverse.
For new libraries, what is the correct convention?
To me it seems like placing the key at the leftmost position makes the most sense semantically. INLINE-wise it also doesn’t seem to matter, arity is pretty much always “all arguments except the data structure(s)”.
An argument could also be made that the key should be at the right next to the data structure, since that composes better, but then insert should look like
Past the data structure itself (Map in the topic, [] in your example) I don’t see this working for anything else. Some may group operations by keys (in which case the key should be on the left), some by the embedded function (key on the right).
In terms of partial application within the library itself, there is indeed a better order, as I said in the first reply, and it is overwhelmingly consistent with what already exists (except for insert, insertWith and singleton).
(I’m assuming you meant fillRangeWith :: (a -> a -> a) -> ... and fillRangeWithKey :: (Int -> a -> a -> a) -> ...)
In this case I see it in terms of function generalization.
For example fillRangeWith generalizes insertWith like this:
insertWith f k v = fillRangeWith f (singletonInterval k) v
This shows clearly which parameters are the keys, which the values, and which are “other”.
And fillRangeWithKey generalizes that further:
fillRangeWith f k v = fillRangeWithKey (const f) k (const v)
Throughout these generalizations there remains a clear connection between the arguments. So I think it is still clear which things are the keys and values.
This also shows one advantage of putting the other at the front, namely that it allows you to define generalizations (that only add other parameters) in a point-free way:
My question is in terms of a new library, I’m definitely not following the (a -> a -> a) convention since that’s just argument duplication (and it’s not even clear about which one of the two arguments is the old one).
insertWith will definitely be generalized the same way as a potential fillRangeWithKey, but that’s simply the correct way to write libraries, not an argument for a specific order.
Other-value-key seems the most consistent to me right now, but I feel like the inverse argument order on insert and singleton would trip people up.
My argument for this specific order (other-key-value-datatype) would be that it is what most people are already used to. I think tradition is a good argument for inconsequential choices like these.
Another argument for keeping the key-value order is that printing the data structure or converting it to a list also puts the contents in that order. That goes all the way back to actual physical dictionaries.
This is getting a bit of topic, but I just thought of this and it might be useful to be aware of: If you use (Int -> a -> a) for the fillRangeWithKey function, then the user might have to do some expensive recomputation, e.g.:
fillRangeWithKey (\k _ -> expensive k) r expensive m
While this link documents Elm practice, I find its arguments very compelling for Haskell, and recommend that the primary data structure should be the final argument to your functions:
Ah that makes sense. The first argument of foldl is a function that expects the main structure type as its first argument. I thought you meant the foldl function itself.