Trying to Wrap a Function with a Datatype
While using the Hakyll framework, I came across a function with the following definition:
renderTagCloudWith :: (Double -> Double ->
String -> String -> Int -> Int -> Int -> String)
-- ^ Render a single tag link
-> ([String] -> String)
-- ^ Concatenate links
-> Double
-- ^ Smallest font size, in percent
-> Double
-- ^ Biggest font size, in percent
-> Tags
-- ^ Input tags
-> Compiler String
-- ^ Rendered cloud
The first function, which I will refer to as renderSingleLink, had 7 types:
Double -> Double -> String -> String -> Int -> Int -> Int -> String) (
I wasn’t sure what each input value represented. After some trial and error I figured out what each of the values where. The input values and output are explained below:
1. Double -> -- Minimum font size as a %
2. Double -> -- Maximum font size as a %
3. String -> -- The tag label
4. String -> -- The tag url
5. Int -> -- The maximum use of the current tag
6. Int -> -- The minimum use of the current tag
7. Int -> -- The maximum use of any tag in the system
8. String -> -- The html representation of the tag (output)
I would have preferred a datatype to encapsulate these values instead of a function with 7 parameters. I was fairly sure I wouldn’t remember what each value meant were I to revisit this code a month from now. Also the first two parameters (Double, Double), were in min-max order. The fifth and sixth parameters (Int, Int), were in max-min order. I felt this lead to unnecessary confusion. As I was using Haskell I assumed this would be quite easy to encapsulate in a datatype.
My first attempt was to create a simple datatype called TagInfo :
data TagInfo = TagInfo {
fontMin :: Double,
fontMax :: Double,
tagName :: String,
tagUrl :: String,
tagMax :: Int,
tagMin :: Int,
maxUseCount :: Int
}
I ordered the parameters to match the order of the renderSingleLink. I thought that I could easily compose the data constructor of TagInfo with a function that provided a String-representation of TagInfo to derive a function that could be supplied to renderTagCloudWith :
showTag :: TagInfo -> String
So basically I wanted to do something like this:
. TagInfo showTag
and pass that composed function to renderTagCloudWith. Unfortunately that does not work. Composing a function that requires one parameter with a function that returns 6 paramaters makes the compiler unhappy!
To clarify, compose (.) is defined as:
(.) :: (b -> c) -> (a -> b) -> a -> c
The constructor of TagInfo is defined as:
TagInfo :: Double -> Double -> String -> String -> Int -> Int -> Int -> TagInfo
the type of showTag is:
showTag :: TagInfo -> String
So composing showTag with TagInfo gives us:
. TagInfo
showTag
Couldn't match type `Double
-> String -> String -> Int -> Int -> Int -> TagInfo'
with `TagInfo'
Expected type: Double -> TagInfo
Actual type: Double
-> Double -> String -> String -> Int -> Int -> Int -> TagInfo
In the second argument of `(.)', namely `TagInfo'
In the expression: showTag . TagInfo
Unfortunately that didn’t work. It seemed so neat to be able to use the TagInfo constructor with showTag to give back the renderSingleLink definition to renderTagCloudWith.
This got me thinking about Scala’s andThen function which is the opposite of compose:
trait Function1[-T1, +R] extends AnyRef { self =>
...
def andThen[A](g: R => A): T1 => A = { x => g(apply(x)) }
}
In Haskell that would be something like:
andThen :: (a -> b) -> (b -> c) -> a -> c
= g $ f x andThen f g x
What I needed was something that could recreate the 7 input parameters needed for the TagInfo constructor. As a first attempt I created andThen7:
andThen7 :: (a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> a8) -> (a8 -> b) -> a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> b
= \x1 x2 x3 x4 x5 x6 x7 -> g $ f x1 x2 x3 x4 x5 x6 x7 andThen7 f g
Now I could do this:
:t (TagInfo `andThen7` showTag)
TagInfo `andThen7` showTag)
( :: Double -> Double -> String -> String -> Int -> Int -> Int -> String
Now the type signature produced in the above matches that required by the renderSingleLink function to renderTagCloudWith.
This is obviously a pretty bad solution. I asked around for a better solution from guys in the BFPG and Mark Hibberd came up with a nested compose as a possible solution (1):
.) .) . ) . ) . ) . ) . TagInfo ((((((showTag
Another solution proposed by Nick Patridge was to use fmap (2):
fmap . fmap . fmap . fmap . fmap . fmap . fmap) showTag TagInfo (
Solution (2) seems like a very nice solution. The type signature of composing fmap is pretty cool and seems to be built for mapping a function into a nested structure:
:t (fmap . fmap . fmap . fmap . fmap . fmap . fmap)
fmap . fmap . fmap . fmap . fmap . fmap . fmap)
( :: (Functor f, Functor f1, Functor f2, Functor f3, Functor f4,
Functor f5, Functor f6) =>
-> b)
(a -> f (f1 (f2 (f3 (f4 (f5 (f6 a))))))
-> f (f1 (f2 (f3 (f4 (f5 (f6 b))))))
I wonder if there is still a better solution? Any thoughts or comments are welcome.