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). Unfortunately, although the
Data.Map API is admirably thorough, it firmly resists partial application.
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):
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
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
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
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
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.