Having based the core of HStringTemplate on Monoids, I started looking for other places to apply them in my code. A natural choice was a string template group. Conceptually, I wanted a very open API — a group is anything you can query for a specific template, be it a data structure, or a wrapper for an unsafe function that accesses a directory, or something that makes a StringTemplate on the fly even. But how do we compose these? I.e., we have two groups, and wish to implement “inheritance” such that one group is queried and if the query fails, then we check for the “higher” group. And most importantly, how do we build this such that any combination of different types of groups can be composed with any other? The first obvious thing to do is to use types to whittle everything down to a basic interface.
From an OO mindset, we would tend to, thinking of an “interface,” use a typeclass with a query function: class Group where query :: Group -> String -> Maybe StringTemplate
. But oy vey! Now we need a function addGroups :: (Group x, Group y) => x -> y -> ???
. Well, to a new concrete group type, I suppose. data ComposedGroup x y => ComposedGroup x y
. And: instance (Group x, Group y) -> Group (ComposedGroup x y) where query s = case query x s of Just a -> Just a; Nothing -> query y s
. Well, this is fine, if a bit bulky. But compose a few groups and a few more and we can see that even if we don’t have to interact with them, the type signatures will be pretty huge. We could avoid this with existential types, but it hardly seems like that improves the elegance of our solution.
There’s a useful insight, however, that I’ve been able to apply at least 50% of the time that I’ve been tempted to reach for existentials — for all practical purposes, a single function typeclass is equivalent to a lambda expression. In fact, all you’re doing when you make such a substitution is rendering the dictionary passing explicit. So switching over to type STGroup = String -> (Maybe StringTemplate)
we see immediate benefits. First, this directly represents what a group is — not even a newtype constructor to get in the way. Second, we can write addGroups x y s = case query x s of Just a -> Just a; Nothing -> query y s
directly, without all the typeclass machinery getting in our way.
Ok, where I’m going with this should be howling obvious by now. We have a bunch of things which have an append operation that is associative, and an obvious zero (const Nothing
) — we have a monoid! There’s actually a slight catch here — we have two monoids, depending on whether the append operation favors the left or right value passed in. And hence, the Data.Monoid library wraps Maybe as newtype First a = First { getFirst :: Maybe a }
, and Last which gives you the last Just value instead of the first. (Side not here: I’d much prefer First was simply the default Monoid instance for Maybe, as Last is its dual [i.e. the same thing with the arguments to mappend reversed], and you as you can wrap any monoid in Dual to get its Dual for free.)
So now we have STGroup = String -> (First StringTemplate)
. And thanks to our old friend the reader monoid [instance Monoid b => Monoid (a -> b)
], we again get the function we really wanted for free: addGroups becomes mappend
!
Another side note: First and Last, absurdly don’t come with built-in functor instances, even though as wrappers for Maybe, the functor instance is obvious: instance Functor First where fmap f x = First . fmap f . getFirst $ x
Now we can “drill down” directly into our group, also for free. So say we have, and we do, a function optInsertTmpl that sets options (as a list of string pairs) for a single template. Now, let’s define a helper function () x y = (fmap . fmap) x y
. [This function should be in everyone’s standard libs by the way. As a generalization from the “owl” which composes a function with two arguments into a function with one argument: o :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c; o = (.).(.)
, I end up using it all the time.] The reader monad (i.e. (String ->
) ) is a functor. We’ve just defined First as a functor. So we can simply write:
optInsertGroup :: [(String, String)] -> Group -> Group optInsertGroup opts f = optInsertTmpl opts <$$> f
We can think of this as “reaching in” two levels deep (past the reader, past the first) and operating directly on every single possible template our group can generate, at once.
Different context than the last example, same conclusion: Get the types and classes right, and the rest of the code almost seems to melt away.