I mean the reader knows Haskell, at least before the monads.
Monad is a type class that allows (informally speaking) of functions that return something like MyMonad Something to perform certain functions that interact with a monad. Functions that use monads need to be linked together by special functions (>>) , (>> =) , (= <<) .
So, Comonad is also a type class that allows functions to interact with the effects of a comonad, only for these functions the comonad is not specified in the returned type, but in the argument. It does not interfere with specifying the same or different comonads in each argument, and even the monad in the return value.
For a bunch of comonad expressions, special functions are also used - (= >>) , (<< =) , (=> =) , (= <=) with their own nuances.
In general, there is a similarity between monads and comonads, and their application can be combined.
In order that it becomes clear we solve a practical example, we will write a mini library to calculate and display the histogram.
Although the task is not very large, we will divide it into parts:
And so, the calculation. There will be neither monads, nor komonads, but it is important. As a result of the preliminary calculation, a function of the type will be formed
type CalcPos x = x -> Maybe Int
For a given initial value, it is the determining number of the histogram pocket, to which one should be added (how the histogram is calculated, and that, it is actually easy to navigate ). If the original number is out of the specified range, the function returns Nothing .
The entire result of the preliminary calculation for the given source data will be stored in the structure
data HistoEngine x = HistoEngine { low :: x -- , step :: x -- , size :: Int -- - , calcPos :: CalcPos x }
The remaining fields of the record do not need the functions of calcPos , it will receive all the data it needs when creating through closures, but other fields may be required in other cases.
The mkHistoEngine function is where the construction of the histogram begins. We take the lower and upper limits of the analyzed range and the number of histogram pockets as the initial data. The mkHistoEngine function fills in and returns HistoEngine , including calcPos given by lambda.
mkHistoEngine :: (Show x, Ord x, RealFrac x) => x -> x -> Int -> HistoEngine x
(the definition of the function is trivial and will be given below, in full code).
Based on the principle of encapsulation ( Gabriel Gonzalez , for example, assumes that comonads are objects , so we don’t disregard one of the principles of the PLO). It would be possible to bring all the above code into a separate module, providing outside access to HistoEngine without its internal content. But then we have to provide functions to get the values of some of the HistoEngine fields.
histoEngPosFn:: HistoEngine x -> CalcPos x histoEngPosFn = calcPos histoEngSize:: HistoEngine x -> Int histoEngSize = size
In the future, you will need to display the histogram scale. Instead of creating access functions for a couple of fields, it is advisable to form the scale values immediately.
gistoXList:: (RealFrac x, Enum x) => HistoEngine x -> [x] gistoXList HistoEngine{..} = take size [low+ step*0.5,(low + step*1.5)..]
(I used the RecordWildCards extension for this {..}).
However, where is the komonad? It's time to go to them.
Komonada will be in the example of the role of a link between the calculation, storage and display. The type of her data I made
data Histogram xa = Histogram (HistoEngine x -> a) (HistoEngine x) deriving Functor
Not every type is suitable for creating a comonad. Not necessarily the same as above: the value and function of this value in an arbitrary type. But the type must allow some recursion with it. In this case, you can recursively call the function HistoEngine x -> a . In other cases, they use recursive data types - lists, trees.
And so, we make Histogram type comonad
instance Comonad (Histogram x) where extract (Histogram fx) = fx duplicate (Histogram fx) = Histogram (Histogram f) x
It's all.
The function extract :: wa -> a (where w is a comonad, the letter w , the accepted notation for comonads is m upside down) corresponds to return in the monad, but does the opposite, returns a value from the comonade. In some articles, it was called coreturn (to call it nruter , it seems, no one thought of it).
This feature will continue to be used directly. Unlike the function duplicate :: wa -> w (wa) , which corresponds to the monads join :: (Monad m) => m (ma) -> ma , only, again, with the opposite meaning. Join deletes one monad layer, and duplicate duplicates the comonade layer. Calling it directly further will not be encountered, but duplicate is called inside other functions of the comonad package, in particular, in extend :: (wa -> b) -> wa -> wb . Actually, a comonad can be defined both through extract , duplicate (and Functor ), and through extract , extend , depending on how convenient it is.
And so, the komonad itself is ready. Go to the next step.
We will store the accumulated values (cells or histogram pockets) in an unbutable vector. Connect
import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM
And write
type HistoCounter = VU.Vector Int type Histo x = Histogram x HistoCounter
It's time to write a function that creates the original comonad-histogram with empty cells.
mkHisto:: (Show x, Ord x, RealFrac x) => x -> x -> Int -> Histo x mkHisto lo hi sz = Histogram (\e -> VU.replicate (histoEngSize e) 0) $ mkHistoEngine lo hi sz
It's simple. We describe a lambda function that selects a vector of a given size filled with zeros and use the previously created mkHistoEngine to create a HistoEngine . The mkHisto parameters are simply passed to mkHistoEngine .
Now we will write the function of accumulation of readings in the histogram.
histoAdd:: x -> Histo x -> HistoCounter histoAdd xh@(Histogram _ e) = case histoEngPosFn ex of Just i -> VU.modify (\v -> VUM.modify v (+1) i) (extract h) Nothing -> extract h
Let me remind you that if a monad function has a monad indicated in the type of result of the function, then in the comonad function it is indicated in the argument. With the help of histoEngPosFn, we get a function of type CalcPos created in mkHistoEngine . Depending on its result, we increase by one the corresponding cell of the vector or return the vector unchanged. The vector itself is obtained by extract h .
In order not to attach the histogram display algorithm to its storage and accumulation, let us assume that for display it must be represented by a list. This is valid, because the list will be processed fewer times and always sequentially to display. (The method of storing the histogram during accumulation can be changed, for example, to a more efficient mutable vector, but this should not lead to changes in the display functions of the histogram).
And so, the komonadnaya function to convert the representation of the data of the histogram from the vector to the list. She is also trivial.
histoToList:: Histo x -> [Int] histoToList = VU.toList . extract
I used the histogram output horizontally, when its bars are on its side.
The function definition is given below, in full code. Note that the function uses both a komonad (in the argument, of course), and a monad (of course, in the output value).
histoHorizPrint:: (RealFloat x, Enum x) => Int -> Int -> Histogram x [Int] -> IO ()
Its arguments are: the width of the output field, the width of the field for displaying the values of the scale of the initial value and, of course, the comonade. Now with data in the form of [Int] instead of VU.Vector Int .
As test data, we will simply generate a sequence of sinusoidal samples.
map sin [0,0.01 .. 314.15]
The histogram will be accumulated in the usual list foldl .
foldl f (mkHisto (-1::Double) 1 20) (map sin [0,0.01 .. 314.16])
(The initial parameters of the histogram - range from -1 to +1, 20 pockets).
But what will be the function f , which, for foldl, has the form (b -> a -> b) , where, in this case, b is a comonade, and is the next, accumulated count. It turns out nothing complicated. After all, we have already written the function histoAdd . Just as for monads, you need auxiliary functions that look like arrows.
And so, the whole test is in the function main .
main :: IO () main = histoHorizPrint 65 5 $ histoToList <<= foldl (\ba -> b =>> histoAdd a) (mkHisto (-1::Double) 1 20) (map sin [0,0.01 .. 314.16])
{-# LANGUAGE DeriveFunctor, RecordWildCards #-} import Control.Comonad import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM import Numeric type CalcPos x = x -> Maybe Int data HistoEngine x = HistoEngine { low :: x -- , step :: x -- , size :: Int -- - , calcPos :: CalcPos x } mkHistoEngine :: (Show x, Ord x, RealFrac x) => x -> x -> Int -> HistoEngine x mkHistoEngine lo hi sz | sz < 2 = error "mkHistoEngine : histogram size must be >1" | lo>=hi = error $ "mkHistoEngine : illegal diapazone " ++ show lo ++ " - " ++ show hi | otherwise = let stp = (hi-lo) / fromIntegral sz in HistoEngine lo stp sz (\x -> let pos = truncate $ (x - lo) / stp in if pos<0 || pos >= sz then Nothing else Just pos) histoEngPosFn:: HistoEngine x -> CalcPos x histoEngPosFn = calcPos histoEngSize:: HistoEngine x -> Int histoEngSize = size gistoXList:: (RealFrac x, Enum x) => HistoEngine x -> [x] gistoXList HistoEngine{..} = take size [low+ step*0.5,(low + step*1.5)..] ------------- ------------------------ data Histogram xa = Histogram (HistoEngine x -> a) (HistoEngine x) deriving Functor instance Comonad (Histogram x) where extract (Histogram fx) = fx duplicate (Histogram fx) = Histogram (Histogram f) x ----------- - histoHorizPrint:: (RealFloat x, Enum x) => Int -> Int -> Histogram x [Int] -> IO () histoHorizPrint width numWidth h@(Histogram _ e) = let l = extract h mx = maximum l width' = width - numWidth - 1 k = fromIntegral width' / fromIntegral mx ::Double kPercent = (100 :: Double) / fromIntegral (sum l) showF w prec v = let s = showFFloat (Just prec) v "" in replicate (w - length s) ' ' ++ s in mapM_ (\(x,y)-> do putStr $ showF numWidth 2 x putStr " " let w = truncate $ fromIntegral y * k putStr $ replicate w 'X' putStr $ replicate (width'-w+2) ' ' putStrLn $ showFFloat (Just 2) (fromIntegral y * kPercent) " %") $ zip (gistoXList e) l ------------- ---- type HistoCounter = VU.Vector Int type Histo x = Histogram x HistoCounter mkHisto:: (Show x, Ord x, RealFrac x) => x -> x -> Int -> Histo x mkHisto lo hi sz = Histogram (\e -> VU.replicate (histoEngSize e) 0) $ mkHistoEngine lo hi sz histoAdd:: x -> Histo x -> HistoCounter histoAdd xh@(Histogram _ e) = case histoEngPosFn ex of Just i -> VU.modify (\v -> VUM.modify v (+1) i) (extract h) Nothing -> extract h histoToList:: Histo x -> [Int] histoToList = VU.toList . extract ------------- main :: IO () main = histoHorizPrint 65 5 $ histoToList <<= foldl (\ba -> b =>> histoAdd a) (mkHisto (-1::Double) 1 20) (map sin [0,0.01 .. 314.16])
Just as for monads, for komonad there are transformers (although there are very few of them, but who prevents us from writing our own). That is, komonads can be nested.
Suppose we want to set the width of the field for outputting numbers at the very beginning, when we have just created an empty histogram.
Connect another module
import Control.Comonad.Env
which implements a komonad within the meaning of the corresponding monad Reader .
The example in the main function will now look like this.
(\h-> histoHorizPrint 65 (ask h) (lower h)) $ (histoToList . lower) <<= foldl (\ba -> b =>> (histoAdd a . lower)) (EnvT 5 (mkHisto (-1::Double) 1 20)) (map sin [0,0.01 .. 314.16])
With the help of (EnvT 5 (...)) we create another comonad layer and store the value 5 in it (the desired width for the field of numbers). Since the comonadal expression is now a two-layer one, the use of our functions using the Histogram comonad requires the auxiliary function lower , just as in the monad transformers, lift is used to go to the previous layer.
Also pay attention to extracting the value - (ask h) . Well, what is not Reader !
The last example can also be written differently by disassembling two layers of comonads at the end (value, internal comonade) using the runEnvT function, the name of which boldly hints at the runReaderT used with ReaderT .
(\(nw,h)-> histoHorizPrint 65 nw h) $ runEnvT $ (histoToList . lower) <<= foldl (\ba -> b =>> (histoAdd a . lower)) (EnvT 5 (mkHisto (-1::Double) 1 20)) (map sin [0,0.01 .. 314.16])
Successes in komonadostroenii!
Source: https://habr.com/ru/post/283368/
All Articles