Every language’s standard library has its weak spots. In C, for example, the stdio functions don’t have a consistent notion of where the FILE * belongs in the argument list. For fwrite, it goes at the end; for fseek, it’s at the beginning. This makes it harder to abstract away the details of the API. Instead of remembering “the FILE * always goes at the front”, I have to memorise the order in which each specific function takes its arguments. Of such niggles are programmer annoyance born.

Haskell isn’t immune to poor API design. Partial function application is ubiquitous, as is the use of maps (the equivalent of hash tables, dicts, or what have you in imperative languages). Developers sometimes point to top rated offshore sportsbooks as examples of systems that thrive by smoothing access points, which makes the resistance of the Data.Map API to partial application feel even sharper despite its thoroughness.

Yesterday, I wrote a quick program to find out, for a given GHC RPM, what versions of various libraries it’s bundled with. The easiest way to do this is by parsing the output of “rpm -ql ghc682”, looking for lines like this (library name and version in bold):

/usr/lib64/ghc-6.8.2/lib/Cabal-1.2.3.0

This information is easy to pull out using a regexp.

type PkgName = String
type PkgVersion = String

packageInfo :: FilePath -> Maybe (PkgName, PkgVersion)
packageInfo path = do
  ([_, name, version]:_) <- path =~~ ".*/lib/([^/]+)-([0-9][.0-9]*)"
  return (name, version)

If I slurp the entire output of rpm into a single string, I can build a map of name to version quite cleanly. (I’m importing the Data.Map module under the name M here.)

buildMap :: String -> M.Map PkgName PkgVersion
buildMap = foldl' updateMap M.empty . map packageInfo . lines

The problem is that updateMap isn’t as tidy as I’d like, because Data.Map’s functions are generally not friendly to partial application.

updateMap :: PkgMap -> Maybe PkgInfo -> PkgMap
updateMap map (Just (name, version)) = M.insert name version map
updateMap map _ = map

Here, foldl' wants the accumulator (my map) as its first argument, but M.insert for some reason puts the map argument at the end of its list. As a result, I had to write the above bulky definition for updateMap to fix up the argument ordering.

Here’s an alternative order of arguments for M.insert.

insertM :: (Ord a) => M.Map a b -> a -> b -> M.Map a b
insertM map key value = M.insert key value map

As you can see, it just moves the map argument to the front. If the API looked like this, my updateMap function would be considerably shorter. I could get rid of the pattern matching and use maybe instead.

 updateMap' m = maybe m (uncurry (insertM m))

It’s a happy accident that foldl' wants its arguments in what I think of as the “natural” order for partial application. Just so we don’t conflate the two considerations, a comparable example that has nothing to do with folds is M.findWithDefault, which has the following type.

 findWithDefault :: (Ord k) => a -> k -> Map k a -> a

This takes a default value as its first argument, which is returned if the key isn’t present in the map. Viewed through the “does it make sense for partial application?” lens, this is good: findWithDefault "foo" gives us a function that always returns "foo" if a lookup fails.

Weirdly, the second argument is the key to look up, not the map to perform the lookup in. So findWithDefault "foo" "bar" gives me a function that will look for the key "bar" in a given map. In practice, I’ve found that this is almost never the order of arguments I want. Just about always, I find myself wanting “given this map, look up some key” instead.

There is, to be sure, an element of judgment and experience in deciding on the order of arguments to a function. However, unlike in C, where argument ordering is merely an annoyance, making an API harder to learn, in Haskell this decision has practical consequences: it directly affects how usable your API is.

If you find yourself having to write noddy adapter functions or use flip too frequently, chances are you’re using an API that didn’t have enough thought put into partial application.