Skip to content

Instantly share code, notes, and snippets.

@joshcough
Last active December 27, 2015 04:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshcough/1c5b77fcd12d6bd410d1 to your computer and use it in GitHub Desktop.
Save joshcough/1c5b77fcd12d6bd410d1 to your computer and use it in GitHub Desktop.

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment