What benefits do I get from creating an instance of Comonad

In my application, I am trying to implement an animation system. In this system, animations are presented in a circular list of frames:

data CyclicList a = CL a [a] 

We can (inefficiently) promote animation as follows:

 advance :: CyclicList a -> CyclicList a advance (CL x []) = CL x [] advance (CL x (z:zs)) = CL z (zs ++ [x]) 

Now I am sure that this data type is comonad:

 instance Functor CyclicList where fmap f (CL x xs) = CL (fx) (map f xs) cyclicFromList :: [a] -> CyclicList a cyclicFromList [] = error "Cyclic list must have one element!" cyclicFromList (x:xs) = CL x xs cyclicLength :: CyclicList a -> Int cyclicLength (CL _ xs) = length xs + 1 listCycles :: CyclicList a -> [CyclicList a] listCycles cl = let helper 0 _ = [] helper n cl' = cl' : (helper (n-1) $ advance cl') in helper (cyclicLength cl) cl instance Comonad CyclicList where extract (CL x _) = x duplicate = cyclicFromList . listCycles 

I have a question: what benefits can I get (if any) from using a comonad instance?

+6
source share
1 answer

The advantage of providing a type class or implementation of an interface is that code written to use that class or interface can use your code without any changes.

What programs can be written in terms of Comonad ? A Comonad provides the ability to both check the value at the current location (without observing its neighbors) using extract , and to observe the neighborhood of each location using duplicate or extend . Without any additional features, this is not very useful. However, if we also need other functions along with the Comonad instance, we can write programs that depend on both local data and data from other sources. For example, if we need functions that allow us to change the location, for example, your advance , we can write programs that depend only on the local data structure, and not on the data structure itself.

For a specific example, consider a cellular automaton program written in Comonad terms and the following Bidirectional class:

 class Bidirectional c where forward :: ca -> Maybe (ca) backward :: ca -> Maybe (ca) 

The program can use this together with Comonad , to extract data stored in the cell, and examine the forward and backward cells of the current cell. It can use duplicate to capture the neighborhood of each cell and fmap to check this neighborhood. This combination of fmap f . duplicate fmap f . duplicate is equal to extract f .

Here is such a program. rule' interesting as an example only; he implements the rules of cellular automata in a neighborhood with left and right values. rule retrieves data from a neighborhood by defining a class and runs a rule in each neighborhood. slice pulls out even larger neighborhoods so we can easily display them. simulate starts the simulation, displaying these larger neighborhoods for each generation.

 rule' :: Word8 -> Bool -> Bool -> Bool -> Bool rule' xlmr = testBit x ((if l then 4 else 0) .|. (if m then 2 else 0) .|. (if r then 1 else 0)) rule :: (Comonad w, Bidirectional w) => Word8 -> w Bool -> w Bool rule x = extend go where go w = rule' x (maybe False extract . backward $ w) (extract w) (maybe False extract . forward $ w) slice :: (Comonad w, Bidirectional w) => Int -> Int -> a -> wa -> [a] slice lraw = sliceL lw (extract w : sliceR rw) where sliceR rw | r > 0 = case (forward w) of Nothing -> take r (repeat a) Just w' -> extract w' : sliceR (r-1) w' sliceR _ _ = [] sliceL lwr | l > 0 = case (backward w) of Nothing -> take l (repeat a) ++ r Just w' -> sliceL (l-1) w' (extract w':r) sliceL _ _ r = r simulate :: (Comonad w, Bidirectional w) => (w Bool -> w Bool) -> Int -> Int -> Int -> w Bool -> IO () simulate flrxw = mapM_ putStrLn . map (map (\x -> if x then '1' else '0') . slice lr False) . take x . iterate f $ w 

This program could be designed to work with the following Bidirectional Comonad , a Zipper on the list.

 data Zipper a = Zipper { heads :: [a], here :: a, tail :: [a] } deriving Functor instance Bidirectional Zipper where forward (Zipper _ _ [] ) = Nothing forward (Zipper lh (r:rs)) = Just $ Zipper (h:l) r rs backward (Zipper [] _ _) = Nothing backward (Zipper (l:ls) hr) = Just $ Zipper ls l (h:r) instance Comonad Zipper where extract = here duplicate (Zipper lhr) = Zipper (goL (h:r) l) (Zipper lhr) (goR (h:l) r) where goL r [] = [] goL r (h:l) = Zipper lhr : goL (h:r) l goR l [] = [] goR l (h:r) = Zipper lhr : goR (h:l) r 

But will also work with CyclicList Bidirectional Comonad .

 data CyclicList a = CL a (Seq a) deriving (Show, Eq, Functor) instance Bidirectional CyclicList where forward (CL x xs) = Just $ case viewl xs of EmptyL -> CL x xs x' :< xs' -> CL x' (xs' |> x) backward (CL x xs) = Just $ case viewr xs of EmptyR -> CL x xs xs' :> x' -> CL x' (x <| xs') instance Comonad CyclicList where extract (CL x _) = x duplicate (CL x xs) = CL (CL x xs) (go (singleton x) xs) where go old new = case viewl new of EmptyL -> empty x' :< xs' -> CL x' (xs' >< old) <| go (old |> x') xs' 

We can reuse simulate with any data structure. CyclicList has a more interesting result, because instead of bumping into a wall, it wraps around to interact with itself.

 {-# LANGUAGE DeriveFunctor #-} import Control.Comonad import Data.Sequence hiding (take) import Data.Bits import Data.Word main = do putStrLn "10 + 1 + 10 Zipper" simulate (rule 110) 10 10 30 $ Zipper (take 10 . repeat $ False) True (take 10 . repeat $ False) putStrLn "10 + 1 + 10 Cyclic" simulate (rule 110) 10 10 30 $ CL True (fromList (take 20 . repeat $ False)) 
+2
source

Source: https://habr.com/ru/post/974854/


All Articles