Skip to the content.

Reader monad and Env comonad

It is said that Env comonad is identical to Reader monad. Then how can you write the same code using Reader and Env?

Let’s take a very simple example that uses Reader.

import Control.Monad
    ( (>>=)
    , (>=>)
    , return
    )
import Control.Monad.Reader
    ( Reader
    , ask
    , runReader
    )

add :: Float -> Reader Int Float
add n = ask >>= \e -> return $ n + fromIntegral e

mul :: Float -> Reader Int Float
mul n = ask >>= \e -> return $ n * fromIntegral e

result, result' :: Float
result = runReader (add 2 >>= mul) 3
result' = runReader ((add >=> mul) 2) 3

How will it look when you write the same code using Env?

import Control.Comonad
    ( (=>>)
    , (=>=)
    , extract
    )
import Control.Comonad.Env
    ( Env
    , ask
    , env
    )

add :: Env Int Float -> Float
add env = fromIntegral (ask env) + extract env

mul :: Env Int Float -> Float
mul env = fromIntegral (ask env) * extract env

result, result' :: Float
result = extract $ env 3 2 =>> add =>> mul
result' = (add =>= mul) $ env 3 2

When you use Reader, each function (add and mul) returns a Reader to lift a value into Reader monad. The bind operator (>>=) passes an environment from a functon to another function.

On the other hand, with Env, each function takes an Env and extract a value from Env comonad. The extend operator (=>>) passes an environment from a functio nto another function.

It’ll help understand what’s going on under the hood by implementing Reader and Env by yourself.

{-# LANGUAGE InstanceSigs #-}

import Control.Monad ((>=>))

newtype Reader e a = Reader { runReader :: e -> a }

instance Functor (Reader e) where
    fmap :: (a -> b) -> Reader e a -> Reader e b
    fmap f (Reader g) = Reader $ f . g

instance Applicative (Reader e) where
    pure x = Reader $ \e -> x
    () :: Reader e (a -> b) -> Reader e a -> Reader e b
    Reader f  Reader x = Reader $ \e -> f e (x e)

instance Monad (Reader e) where
    (>>=) :: Reader e a -> (a -> Reader e b) -> Reader e b
    Reader x >>= f = Reader $ \e -> let Reader y = f (x e) in y e

ask :: Reader e e
ask = Reader id

>>= applies the same environment to the input (x) and the output (y).

{-# LANGUAGE InstanceSigs #-}

import Control.Comonad
    ( Comonad
    , (=>>)
    , (=>=)
    , extract
    , extend
    )

newtype Env e a = Env (e, a)

instance Functor (Env e) where
    fmap f (Env (e, x)) = Env (e, f x)

instance Comonad (Env e) where
    extract :: Env e a -> a
    extract (Env (_, a)) = a
    extend :: (Env e a -> b) -> Env e a -> Env e b
    extend f env@(Env (e, x)) = Env (e, f env)

ask :: Env e a -> e
ask (Env (e, _)) = e

extend takes out the environment from the input (e) and uses it to evaluate the function (f), and uses the same environment to create a return value too.