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 |