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.