Think of it like this: when you use cobind with a comonad you provide some function that distills down a comonadic data structure into a single value and then cobind applies this distillation repeatedly at every point in the comonadic structure to give you another comonadic structure. My canonical example is the cellular automaton where you give a rule that defines how to compute each individual cell as a function of the whole grid, and cobind applies this throughout an entire input grid to give you back an entire output grid. The functions cfix and loeb allow you to give rules for each cell where you're allowed to make reference to the entire output grid. If you think of the output grid as something in the future that you haven't yet computed, then cfix and loeb allow you to read from the future.
Here's an example of cfix it in use (where I make [] a comonad by interpreting it as a kind of one-sided zipper):
> instance Show (x -> a)
> instance Eq (x -> a)
> instance (Num a,Eq a) => Num (x -> a) where
> fromInteger = const . fromInteger
> f + g = \x -> f x + g x
> f * g = \x -> f x * g x
> negate = (negate .)
> abs = (abs .)
> signum = (signum .)
> class Comonad w where
> coreturn :: w a -> a
> cobind :: (w a -> b) -> w a -> w b
> instance Comonad [] where
> coreturn (x:xs) = x
> cobind f [] = []
> cobind f (x:xs) = f (x:xs) : cobind f xs
> cfix :: Comonad d => d (d a -> a) -> a
> cfix d = coreturn d (cobind cfix d)
> ouroboros = [2*head.tail,1+head.tail,17]
> test = cfix ouroboros
In the expression test we can use head and tail to refer to the sublist to the right of each element from within the list itself. For example, in the subexpression 2*head.tail the head.tail is being applied to cfix [1+head.tail,17].
Note how in this example (1) loeb gives you back an entire list, not just one element and (2) cfix respects the comonadic structure in the sense that you can only refer to elements to the right of the current one whereas loeb lets you refer to the entire output list. I think this helps to make clear the differences and similarities between cfix and loeb.
That wasn't too brief. But I did cheat by copying and pasting code from an old post. Anyway, back to tinkering with my implementation of dropWhile...
Your blog continues to amaze me, and I just wanted to say thanks. Most of the CompSci jargon is gobbledygook to me, but I really appreciate the effort you're taking to talk about monads and the like.
ReplyDeletekea,
ReplyDeleteIf you think some of the jargon looks like gobbledygook, you should check out the published papers that some of this stuff comes from :-) It's been hard work trying to make inroads into this stuff over the last two years.
That's a nice contrast you've developed.
ReplyDeleteMy first post was inspired mostly by type signatures. Now that I've looked at your actual definition of loeb :), I readily see the difference you're highlighting. As you pointed out, loeb does not need the comonadic structure. This highlights the big difference: comonadic structure (and thus cfix) caters to relative references [1] whereas loeb allows for absolute references.
In your spreadsheet example, you use !! to get the nth cell in the list. A comonadic approach would allow a cell to refer to "the cell to my left" without needing to know its own cell number. Both features are supported in various ways by spreadsheets: whenever I do a fill-down in Excel, every now and then I'm surprised that the relative references worked how I had intended.
Just a couple of notes: to get cfix to "return a whole list" you can use cobind cfix. Also, to have access to the "entire output list", you can use a more robust comonad (like Uustalu and Vene's LVS) with an absolute access function a la !!.
Thanks for the discussion, that was a nice contrast to appreciate.
[1] - This is why zippers induce comonads; I can talk about neighbors in various directions relative to wherever I'm currently at in the structure.
Forgot to sign! Nick Frisby here again :)
ReplyDelete(I guess kotsu=Nick Frisby.)
ReplyDeleteOf course! cfix and loeb deal with relative and absolute references. I hadn't thought of it that way. It's truly bizarre how fiddling about semi-randomly with types leads to new ways to view concepts that are already well known and considered natural.
I'll have a look at Uustalu and Vene's LVS. Their recent papers have been really interesting.
I've been reading your explorations of comonads -- your explanations using examples of cellular automata and image processing have been very helpful for me to understand them a bit better. In parallel, I've been reading about Haskell attribute grammars. Do comonads address a class of problems well that attribute grammars do not?
ReplyDeleteOne of the issues with monads, even with transformers, is that they "stack" poorly (the result depends on order of composition and each monad's internal implementation detail); do comonads compose better than monads?
geophf,
ReplyDeleteBy coincidence, I've been thinking about stacking comonads for the last couple of days. Not got very far yet. A web search reveals almost no hits on "comonad transformer".
re: your thinking about composing comonads -- excellent, as I've observed that you thinking about classes of problems results in solutions on your blog.
ReplyDeleteAs to attribute grammars for Haskell, have you looked at them? Applied them? Do you see them being useful in these problem domains, or do they address an entirely different class of problems?