Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there significance in the order of Haskell function parameters?

Tags:

haskell

I've been learning Haskell and I noticed that many of the built in functions accept parameters in an order counter intuitive to what I would expect. For example:

replicate :: Int -> a -> [a]

If I want to replicate 7 twice, I would write replicate 2 7. But when read out loud in English, the function call feels like it is saying "Replicate 2, 7 times". If I would have written the function myself, I would have swapped the first and second arguments so that replicate 7 2 would read "replicate 7, 2 times".

Some other examples appeared when I was going through 99 Haskell Problems. I had to write a function:

dropEvery :: [a] -> Int -> [a]`

It takes a list as its first argument and an Int as its second. Intuitively, I would have written the header as dropEvery :: Int -> [a] -> [a] so that dropEvery 3 [1..100] would read as: "drop every third element in the list [1..100]. But in the question's example, it would look like: dropEvery [1..100] 3.

I've also seen this with other functions that I cannot find right now. Is it common to write functions in such a way due to a practical reason or is this all just in my head?

like image 819
aanrv Avatar asked Jul 31 '15 00:07

aanrv


2 Answers

It's common practice in Haskell to order function parameters so that parameters which "configure" an operation come first, and the "main thing being operated on" comes last. This is often counter intuitive coming from other languages, since it tends to mean you end up passing the "least important" information first. It's especially jarring coming from OO where the "main" argument is usually the object on which the method is being invoked, occurring so early in in the call that it's out of the parameter list entirely!

There's a method to our madness though. The reason we do this is that partial application (through currying) is so easy and so widely used in Haskell. Say I have a functions like foo :: Some -> Config -> Parameters -> DataStrucutre -> DataStructure and bar :: Differnt -> Config -> DataStructure -> DataStructure. When you're not used to higher-order thinking you just see these as things you call to transform a data structure. But you can also use either of them as a factory for "DataStructure transformers": functions of the type DataStructure -> DataStructure.

It's very likely that there are other operations that are configured by such DataStructure -> DataStructure functions; at the very least there's fmap for turning transformers of DataStructures into transformers of functors of DataStructures (lists, Maybes, IOs, etc).

We can take this a bit further sometimes too. Consider foo :: Some -> Config -> Parameters -> DataStructure -> DataStructure again. If I expect that callers of foo will often call it many times with the same Some and Config, but varying Parameters, then even-more-partial applications become useful.

Of course, even if the parameters are in the "wrong" order for my partial application I can still do it, using combinators like flip and/or creating wrapper functions/lambdas. But this results in a lot of "noise" in my code, meaning that a reader has to be able to puzzle out what is the "important" thing being done and what's just adapting interfaces.

So the basic theory is for a function writer to try to anticipate the usage patterns of the function, and list its arguments in order from "most stable" to "least stable". This isn't the only consideration of course, and often there are conflicting patterns and no clear "best" order.

But "the order the parameters would be listed in an English sentence describing the function call" would not be something I would give much weight to in designing a function (and not in other languages either). Haskell code just does not read like English (nor does code in most other programming languages), and trying to make it closer in a few cases doesn't really help.

For your specific examples:

  1. For replicate, it seems to me like the a parameter is the "main" argument, so I would put it last, as the standard library does. There's not a lot in it though; it doesn't seem very much more useful to choose the number of replications first and have an a -> [a] function than it would be to choose the replicated element first and have an Int -> [a] function.

  2. dropEvery indeed seems to take it's arguments in a wonky order, but not because we say in English "drop every Nth element in a list". Functions that take a data structure and return a "modified version of the same structure" should almost always take the data structure as their last argument, with the parameters that configure the "modification" coming first.

like image 70
Ben Avatar answered Jan 01 '23 20:01

Ben


To add to the other answers, there's also often an incentive to make the last argument be the one whose construction is likely to be most complicated and/or to be a lambda abstraction. This way one can write

f some little bits $
  big honking calculation
  over several lines

rather than having the big calculation surrounded by parentheses and a few little arguments trailing off at the end.

like image 23
dfeuer Avatar answered Jan 01 '23 20:01

dfeuer