Какие преимущества я получу от создания экземпляра Comonad
В моем приложении я пытаюсь реализовать систему анимации. В этой системе анимации представлены в виде циклического списка кадров:
data CyclicList a = CL a [a]
Мы можем (неэффективно) продвигать анимацию следующим образом:
advance :: CyclicList a -> CyclicList a
advance (CL x []) = CL x []
advance (CL x (z:zs)) = CL z (zs ++ [x])
Теперь я уверен, что этот тип данных является comonad:
instance Functor CyclicList where
fmap f (CL x xs) = CL (f x) (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
У меня такой вопрос: какие преимущества я получаю (если таковые имеются) от использования экземпляра comonad?
1 ответ
Преимущество предоставления класса типов или реализации интерфейса состоит в том, что код, написанный для использования этого класса типов или интерфейса, может использовать ваш код без каких-либо изменений.
Какие программы могут быть написаны с точки зрения Comonad
? Comonad
обеспечивает способ проверки значения в текущем местоположении (без наблюдения за соседями), используя extract
и способ наблюдать окрестности каждого места с duplicate
или же extend
, Без каких-либо дополнительных функций это не очень полезно. Однако, если нам также потребуются другие функции наряду с Comonad
Например, мы можем писать программы, которые зависят как от локальных данных, так и от данных из других источников. Например, если нам требуются функции, которые позволяют нам менять местоположение, например, ваше advance
Мы можем писать программы, которые зависят только от локальной структуры данных, а не от самой структуры данных.
Для конкретного примера рассмотрим программу клеточных автоматов, написанную в терминах Comonad
и следующее Bidirectional
учебный класс:
class Bidirectional c where
forward :: c a -> Maybe (c a)
backward :: c a -> Maybe (c a)
Программа может использовать это вместе с Comonad
, чтобы extract
данные хранятся в ячейке и исследовать ячейки forward
а также backward
текущей ячейки. Можно использовать duplicate
чтобы захватить окрестности каждой клетки и fmap
осмотреть этот район. Это сочетание fmap f . duplicate
является extract f
,
Вот такая программа. rule'
интересен только пример; он реализует правила клеточных автоматов по соседству только с левым и правым значениями. rule
извлекает данные из окрестности, учитывая класс, и запускает правило для каждой окрестности. slice
вытаскивает еще большие кварталы, чтобы мы могли легко их отобразить. simulate
запускает симуляцию, отображая эти большие окрестности для каждого поколения.
rule' :: Word8 -> Bool -> Bool -> Bool -> Bool
rule' x l m r = 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 -> w a -> [a]
slice l r a w = sliceL l w (extract w : sliceR r w)
where
sliceR r w | r > 0 = case (forward w) of
Nothing -> take r (repeat a)
Just w' -> extract w' : sliceR (r-1) w'
sliceR _ _ = []
sliceL l w r | 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 f l r x w = mapM_ putStrLn . map (map (\x -> if x then '1' else '0') . slice l r False) . take x . iterate f $ w
Эта программа могла быть предназначена для работы со следующими Bidirectional
Comonad
, Zipper
в списке.
data Zipper a = Zipper {
heads :: [a],
here :: a,
tail :: [a]
} deriving Functor
instance Bidirectional Zipper where
forward (Zipper _ _ [] ) = Nothing
forward (Zipper l h (r:rs)) = Just $ Zipper (h:l) r rs
backward (Zipper [] _ _) = Nothing
backward (Zipper (l:ls) h r) = Just $ Zipper ls l (h:r)
instance Comonad Zipper where
extract = here
duplicate (Zipper l h r) = Zipper (goL (h:r) l) (Zipper l h r) (goR (h:l) r)
where
goL r [] = []
goL r (h:l) = Zipper l h r : goL (h:r) l
goR l [] = []
goR l (h:r) = Zipper l h r : goR (h:l) r
Но также будет работать с 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'
Мы можем использовать повторно simulate
с любой структурой данных. CyclicList
имеет более интересный результат, потому что вместо того, чтобы врезаться в стену, он оборачивается, чтобы взаимодействовать с самим собой.
{-# 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))