Completing a Comonad example

As of March 2020, School of Haskell has been switched to read-only mode.

Hi! This is about completing an example from a Gabriel Gonzalez' splendid article Comonads are objects, section "The command pattern" that I couldn't make it work.

With the Comonad extend method, you can turn a getter method (w a -> b) into an object transformer (w a -> w b), and thus you may use them to sequence multiple transformations. The object type must be a Functor instance (Comonads are Functors).

Rewriting the example

The object type in the article was (Kelvin, Kelvin -> a), but using it like this, the Comonad instance taken by the compiler was the Tuple-2 one where the inner type is its second element one, and the example didn't compile.

Defining a newtype around it, with instances for Functor and Comonad, makes the example work.

Using the "stack" template "simple", paste the following on Main.hs

{-# LANGUAGE PackageImports, GeneralizedNewtypeDeriving, FlexibleInstances #-}

module Main where

import Data.Function ((&))   -- (&): backwards application
import "HUnit" Test.HUnit

import "comonad" Control.Comonad (Comonad(..))

-- 

newtype Kelvin = Kelvin { getKelvin :: Double } deriving (Num, Fractional, Show)

newtype Celsius = Celsius { getCelsius :: Double }
    deriving (Num, Fractional, Show)


-- wrapping the (data, function) pair

newtype Thermostat a = Thermo (Kelvin, Kelvin -> a)

instance Functor Thermostat where

    -- fmap :: (a -> b) -> w a -> w b
    
    fmap g (Thermo (k, f)) = Thermo (k, g . f)


instance Comonad Thermostat where

    -- the dual of monad's return
    extract (Thermo (k, f)) = f k
    
    -- the dual of join
    duplicate (Thermo (k, f)) = Thermo (k, \k' -> Thermo (k', f))
    
    -- laws:
    -- extract . duplicate = id
    -- fmap extract . duplicate = id
    -- duplicate . duplicate = fmap duplicate . duplicate

---

kelvinToCelsius :: Kelvin -> Celsius
kelvinToCelsius (Kelvin t) = Celsius (t - 273.15)

initialThermostat :: Thermostat Celsius
initialThermostat = Thermo (298.15, kelvinToCelsius)

up :: Thermostat a -> a
up (Thermo (t, f)) = f (t + 1)

down :: Thermostat a -> a
down (Thermo (t, f)) = f (t - 1)

toString :: Thermostat Celsius -> String
toString (Thermo (t, f)) = show (getCelsius (f t)) ++ " Celsius"

mymethod :: Thermostat Celsius -> String
mymethod obj = obj 
                  & extend up 
                  & extend up 
                  & toString

test1 = TestCase $ assertEqual "for the initial obj.:" (initialThermostat & toString) "25.0 Celsius"
test2 = TestCase $ assertEqual "for the extended obj.:" (initialThermostat & mymethod) "27.0 Celsius"

tests = TestList [TestLabel "test1" test1, TestLabel "test2" test2]

main :: IO Counts
main = runTestTT tests

Then add the packages comonad and HUnit to the project .cabal file.

$ stack exec myproject
Cases: 2  Tried: 2  Errors: 0  Failures: 0

Observing types and the extend up transformation

Using ghci we can show the types of applying the Comonad extend method to the getters:

$ stack ghci
GHCi, version 8.4.4: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main ...  
Ok, one module loaded.
Loaded GHCi configuration from ...

*Main> :t up
up :: Thermostat a -> a

*Main> :t extend up
extend up :: Thermostat b -> Thermostat b

*Main> :t toString
toString :: Thermostat Celsius -> String

*Main> :t extend toString
extend toString :: Thermostat Celsius -> Thermostat String

*Main> 

Let's see how extend up transforms Thermo (k, f) through its simplification:


extend up $ Thermo (k, f)

-- since `extend f = fmap f . duplicate`

fmap up $ duplicate $ Thermo (k, f)

-- from the Comonad instance

fmap up $ Thermo (k, \k' -> Thermo (k', f))

-- from the Functor instance

 Thermo (k, up . (\k' -> Thermo (k', f)))
 
 Thermo (k, \k' -> up (Thermo (k', f)))
 
 -- from the definition of `up`

 Thermo (k, \k' -> f (k' +1))

Checking the Comonad laws on our instance

I follow the second group of Comonad laws over duplicate based definitions

Let's check extract . duplicate = id

extract $ duplicate $ Thermo (k, f)

-- from the Comonad instance

extract $ Thermo (k, \k' -> Thermo (k', f))

(\k' -> Thermo (k', f)) k

Thermo (k, f)

Checking fmap extract . duplicate = id


fmap extract $ duplicate $ Thermo (k, f)

-- from the Comonad instance

fmap extract $ Thermo (k, \k' -> Thermo (k', f))

-- from the Functor instance

Thermo (k, extract . (\k' -> Thermo (k', f)))

Thermo (k, \k' -> extract (Thermo (k', f)))

-- from the Comonad instance

Thermo (k, \k' -> f k')

-- eta reduction

Thermo (k, f)

Checking duplicate . duplicate = fmap duplicate . duplicate

  • one side:

fmap duplicate $ duplicate $ Thermo (k, f)

-- from the Comonad instance

fmap duplicate $ Thermo (k, \k' -> Thermo (k', f))

-- from the Functor instance

Thermo (k, \k' -> duplicate (Thermo (k', f)))

Thermo (k, \k' -> Thermo (k', \k'' -> Thermo (k'', f)))
  • and the other:

duplicate $ duplicate $ Thermo (k, f)

duplicate $ Thermo (k, \k' -> Thermo (k', f))

-- let's name `(\k' -> Thermo (k', f))` as `g`

duplicate $ Thermo (k, g)

Thermo (k, \k' -> Thermo (k', g))

-- substituting g

Thermo (k, \k' -> Thermo (k', \k'' -> Thermo (k'', f)))

Looking at some Monad / Comonad symmetry

Monad... Comonad
return:: a -> m a extract:: w a -> a
join:: m (m a) -> m a duplicate:: w a -> w (w a)
(>>=):: m a -> (a -> m b) -> m b extend:: (w a -> b) -> w a -> w b
(>>= f)= join . fmap f extend f= fmap f . duplicate