Motivation
I have some code which is presently run for its side effects, but I’d like it data too. It’s analogous to extending writeFile to return the size of the contents:
writeFileAndCount :: FilePath -> String -> IO Int
writeFileAndCount path contents = do
writeFile path contents
return $ length contents
All well and good! Now we could just have two functions, one which returns IO () and the other which returns IO Int, but it seems a shame to pollute the namespace. Instead it would be nice if we could say e.g.:
ghci> writeFile' "foo.txt" "Hello" :: IO ()
ghci> writeFile' "foo.txt" "Hello" :: IO Int
5
Happily we can!
A mathematical analogy
You’ve probably already seen code whose return type changes to match the context. For example in Haskell’s maths libraries many functions will return either Float or Double. Here’s sqrt:
ghci> (sqrt 2) :: Float
1.4142135
ghci> (sqrt 2) :: Double
1.4142135623730951
To see how this works, look at the type of sqrt:
ghci> :t sqrt
sqrt :: Floating a => a -> a
There’s no mention of Double or Float there. Instead, we see that sqrt will work with any instance of the Floating typeclass.
Under the covers we’d expect different instances of sqrt: one Double -> Double, another Float -> Float. Having inferred the relevant type, the compiler will then pick the particular instance we need.
Conceptually we might have:
sqrtD :: Double -> Double
sqrtF :: Float -> Float
sqrt :: (Floating a) => a -> a
Note that all of these functions don’t change the type: we can’t implicitly change e.g. a Float to a Double:
ghci> (sqrt (2 :: Float)) :: Double
<interactive>:5:8:
Couldn't match expected type ‘Double’ with actual type ‘Float’
In the first argument of ‘sqrt’, namely ‘(2 :: Float)’
In the expression: (sqrt (2 :: Float)) :: Double
In an equation for ‘it’: it = (sqrt (2 :: Float)) :: Double
This is because the signature has just one degree-of-freedom:
sqrt :: a -> a
rather than
sqrt :: a -> b
A polymorphic wrapper
Having seen that sqrt can choose different code in different contexts, let’s try to write a combinator which either passes a value unchanged, or converts it to ().
By analogy with sqrt consider combining:
toId :: a -> a
toUnit :: a -> ()
Although the first equation both accepts and returns the same type, the second doesn’t. So it makes sense to invent a type class with two parameters. On a technical level, this means we’ll need the MultiParamTypeClasses1 GHC extension.
Here’s a suitable type class:
class OrUnit b a where
orUnit :: a -> b
We also need a couple of instances:
instance OrUnit () a where
orUnit a = ()
instance OrUnit a a where
orUnit a = a
In the first instance above, the () is a concrete type rather than a variable, so we’ll also need the FlexibleInstances2 extension.
Given this, we can write things like this:
ghci> orUnit 'a' :: Char
'a'
ghci> orUnit 'a' :: ()
()
or indeed our original goal of writeFile':
writeFile' :: (OrUnit a Int) => FilePath -> String -> IO a
writeFile' path contents = liftM orUnit
$ writeFileAndCount path contents
We need liftM to lift orUnit into the IO Monad.
Problems
Although we’ve met our original goal of adding optional return data from a function whilst keeping compatibility with old code, it isn’t perfect.
We saw above that we need GHC extensions to compile the module. Sadly we also need to enable extensions when using it:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
So, it’s doesn’t give a completely backwards-compatible way to extend the original API.
Also the extra flexibility we enjoy seems to force us to specify explicit types more often. Perhaps other GHC extensions would help here.
The code
You can grab the code from GitHub3. It’s just for fun, and consequently I’ve not uploaded it to hackage.
References
- 1. https://wiki.haskell.org/Multi-parameter_type_class
- 2. http://connectionrequired.com/blog/2009/07/my-first-introduction-to-haskell-extensions-flexibleinstances/
- 3. https://github.com/mjoldfield/or-unit
![Atom Feed [ Atom Feed ]](../../atom.png)