On the argument order convention in containers

containers has an informal convention going on:

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

insert :: Ord k => a -> k -> Map k a -> Map k a

How will your function be partially applied? Order parameters accordingly.

Imagine having map as [a] -> (a -> a) -> [a], having to write flip map f every time would be cumbersome.

1 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).

So key-other-value-datatype or key-value-other-datatype?

I personally like the other-key-value-datatype order and see no reason to change it for new libraries.

What about this pair of functions?

fillRange :: (a -> a) -> Interval -> a -> IntMap a -> IntMap a

fillRangeWithKey :: (Int -> a -> a) -> Interval -> (Int -> a) -> IntMap a -> IntMap a

a.k.a. insertWith on an interval

Or should I say “at which point does a value become other”?

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

insert = insertWith const
2 Likes

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:

https://package.elm-lang.org/help/design-guidelines#the-data-structure-is-always-the-last-argument

Unfortunately Haskell’s foldl and foldl’ want the primary data structure first.

Surely the primary structure of foldl is the list, which is the last argument. Don’t you agree?

2 Likes

No, I was referring to the need for flip in

foldl' (flip Set.delete) s [3,1,4]

Why was I bringing this up? Because Elm’s foldl was brought up, but Elm’s foldl has a different calling convention.

2 Likes

…because the designers of Elm decided to look here first?

https://wiki.haskell.org/Nitpicks

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.