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 MultiParamTypeClasses
1 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 FlexibleInstances
2 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