Let's say I want to look something up in an environment (like Map String Int
or something, doesn't really matter). I start with Reader for the Env,
and I use the ask
function.
type Env = Map String Int
envLookup :: String -> Reader Env Int
envLookup key = (Maybe.fromMaybe (error "unbound var") . Map.lookup key) <$> ask
I have an obvious problem here, the error in the fromMaybe call. I could solve it like this:
envLookupMaybe :: String -> Reader Env (Maybe Int)
envLookupMaybe key = Map.lookup key <$> ask
But I don't like that for some reason. First, I don't want to return a Maybe
.
This forces the caller to deal with the problem, but they may have lost information
about what the problem was. I can at least change that to Either
.
envLookupEither :: String -> Reader Env (Either String Int)
envLookupEither key = (maybe (Left $ "unbound var" ++ key) return . Map.lookup key) <$> ask
That's pretty reasonable. But, I can do better.
I want to be generic, so I lean on the power of Haskell, and ask
.
After adding some imports (which you can see below), I type in this:
envLookupError key =
(maybe (throwError "unbound var") return . Map.lookup key) <$> ask
Here's the thing...I'm not sure yet what type this gives me. But I ask ghci:
*Main> :t envLookupError
envLookupError :: (Ord k, MonadReader (Map k a) f, MonadError [Char] m) =>
k -> f (m a)
Okay, this seems a bit magical. What is this telling me? Let's see if we can make it more concrete.
What's something that is a MonadReader
? And how about MonadError
?
I dig around a bit in the docs (the links are now eluding me, but I'll find them and paste them here later)
and find some things that I'm familiar with. Reader
is a pretty obvious candidate for MonadReader
,
and I bet there's a MonadError
instance for Either
.
So what happens if I replace MonadError [Char] m
with the concrete type Either String Int
?
envLookupError' :: MonadReader Env m => String -> m (Either String Int)
envLookupError'= envLookupError
Magic - it just works. Okay, so what if I try it the other way around?
What happens if I replace MonadReader (Map k a) f
with Reader Env
?
envLookupReader :: MonadError String m => String -> Reader Env (m Int)
envLookupReader = envLookupError
Magic - it just works again. I'm beginning to understand that it isn't magic. The type ghci gave me for the code I typed in originally was simply the most generic possible type for that code. I can nail down any of the abstract types that I want, whenever I want, fixing them to concrete types.
Let's do one more, nailing down both.
envLookupReaderEither :: String -> Reader Env (Either String Int)
envLookupReaderEither = envLookupError
Success again. So back to that original type:
*Main> :t envLookupError
envLookupError :: (Ord k, MonadReader (Map k a) f, MonadError [Char] m) =>
k -> f (m a)
What is it telling us? It's saying that we can plug in any MonadReader (Map k a)
instance,
and/or any MonadError
instance, at any time we please.
If we plug in a MonadReader
instance like Reader Env
, then instead of a final
result of f (m a)
, we'll get a Reader Env (m Int)
. If we plug in a MonadError String
instance like Either String
, then instead of the final result of f (m a)
, we'll get a
f (Either String a)
. Plugging them both in gives us Reader Env (Either String Int)
.
I have a lot more to write about this, especially examples of actually using the newly created functions, since I'm guessing if you're reading this for the first time, it's still abstract and fuzzy.
So, more to come soon. But for now, full Contents below:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
import Control.Applicative
import Control.Monad.Except
import Control.Monad.Trans.Except
import Control.Monad.Reader
import Data.Functor.Identity
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
type Env = Map String Int
-- Let's say I want to look something up in an environment (Map String Int)
-- or something, doesn't really matter. I start with Reader for the Env,
-- and I use ask.
envLookup :: String -> Reader Env Int
envLookup key = (Maybe.fromMaybe (error "unbound var") . Map.lookup key) <$> ask
-- I have an obvious problem here, the fromMaybe. I could solve it like this:
envLookupMaybe :: String -> Reader Env (Maybe Int)
envLookupMaybe key = Map.lookup key <$> ask
-- But I don't like that. I want to be generic, so here is my solution:
envLookupError :: (Ord k, MonadReader (Map k a) f, MonadError [Char] m) =>
k -> f (m a)
envLookupError key =
(maybe (throwError "unbound var") return . Map.lookup key) <$> ask
envLookupReaderTEither :: Monad m => String -> ReaderT Env m (Either String Int)
envLookupReaderTEither = envLookupError
envLookupReader :: MonadError String m => String -> Reader Env (m Int)
envLookupReader = envLookupError
envLookupReaderEither :: String -> Reader Env (Either String Int)
envLookupReaderEither = envLookupError
envFull = Map.singleton "x" 5
envEmpty = Map.empty
e1 :: String
e1 = either id show $ runReader (envLookupError "x") envFull
e2 :: Maybe (Either String Int)
e2 = runReaderT (envLookupError "x") envFull
e2' :: [(Either String Int)]
e2' = runReaderT (envLookupError "x") envEmpty
e3 :: String
e3 = maybe "hmm" (either id show) $ runReaderT (envLookupReaderTEither "x") envFull
e4 :: String
e4 = either id show $ runReader (envLookupReader "x") envEmpty
e5 :: Int
e5 = either (const (-1)) id $ runReader (envLookupReaderEither "x") (Map.singleton "x" 6)