So I said yesterday that I really wanted to find a better way to do
our by-hand mapping of XML-RPC structs, because doing it by
hand—and, specifically, repeating a bunch of information multiple
times—was tedious, error-prone and ugly. Here's a smaller struct
we're working with, for WPCustomField
—smaller, but it's still a
bunch of boilerplate:
data WPCustomField = WPCustomField {
cfId :: String,
cfKey :: String,
cfValue :: String
} deriving Show
instance XmlRpcType WPCustomField where
toValue struct = toValue $ [("id", toValue (cfId struct)),
("key", toValue (cfKey struct)),
("value", toValue (cfValue struct))]
fromValue v = do
struct <- fromValue v
a <- getField "id" struct
b <- getField "key" struct
c <- getField "value" struct
return WPCustomField {
cfId = a,
cfKey = b,
cfValue = c }
getType _ = TStruct
So I started thinking about it. It seemed obvious to me that I would want to start with a list of tuples—each tuple establishing a mapping from XML-RPC attribute name to accessor function, and put in a list because I was going to need to keep their ordering in order to feed them to the data constructor in the proper order.
So I did this:
cfMapping = [("id", cfId),
("key", cfKey),
("value", cfValue)];
This seemed simple enough that I didn't bother to write a type declaration, or let ghc-mod do it for me—in which case I might have seen the upcoming problem.
At first I thought my biggest limitation was going to be the fact that I couldn't see a way to transform the fromValue function—while I could map over the entries in cfMapping, I didn't see how I was going to be able to take the resulting list and give it to the WPCustomField data constructor.
Then it hit me—I could fold over the list, and partially apply each value to the Data constructor, so that when we got to the end of the list, we'd have an actual value.
Boy did I feel proud of that solution.1
Figuring that I had that problem licked, I decided to first rewrite
the toValue function for the WPCustomField
structure. I wrote a
function that would map over our mapping list, and return the sort of
list we were looking for:
mapToValue mapping struct = toValue $ map aListToValue mapping
where aListToValue (key, accessor) = (key, toValue (accessor struct))
By making sure that the struct
was the last thing handed in, I even
got to write the new toValue
function in a point-free style:
toValue = mapToValue cfMapping
So I compiled it and it ran. "Great!" I thought. "This is going to be
easy!" And then I hit WPEnclosure
:
data WPEnclosure = WPEnclosure {
eUrl :: String,
eLength :: Int,
eType :: String
} deriving Show
Many of you will see what is wrong immediately. I hinted at the
direction of the problem when I mentioned that I didn't bother to
write a type signature for the cfMapping
list—because once you see
it, and look at WPEnclosure, I think it becomes obvious what the
problem is:
cfMapping :: [(String, WPCustomField -> String)]
That's right—the eLength
field being an Int
among String
fields means that we've got heterogenous tuple types for the
WPEnclosure
type. FAIL.
So, for the moment I'm going to put this cleanup on hold, and just proceed with the hand-rolled instances.
In retrospect, I see that this wouldn't work, because the types of each of those partially applied functions would not be the same, so the accumulator couldn't typecheck. Oh, well, pride goeth before the fall and all that.