Kenneth Foner - Getting a Quick Fix on Comonads

Embed Size (px)

Citation preview

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    1/12

    Functional Pearl: Getting a Quick Fix on Comonads

    Kenneth Foner

    University of Pennsylvania, USA

    [email protected]

    Abstract

    A piece of functional programming folklore due to Piponi providesLobs theorem from modal provability logic with a computationalinterpretation as an unusual fixed point. Interpreting modal necessityas an arbitraryFunctorin Haskell, the type of Lobs theorem isinhabited by a fixed point function allowing each part of a structureto refer to the whole.

    However,Functors logical interpretation may be used to proveLobs theorem only by relying on its implicit functorial strength,

    an axiom not available in the provability modality. As a result, thewell knownloebfixed point cheats by using functorial strength toimplement its recursion.

    Rather thanFunctor, a closer Curry analogue to modal logicsHoward inspiration is a closed (semi-)comonad, of which HaskellsComonadApplytypeclass provides analogous structure. Its compu-tational interpretation permits the definition of a novel fixed pointfunction allowing each part of a structure to refer to its own contextwithin the whole. This construction further guarantees maximal shar-ing and asymptotic efficiency superior toloebfor locally contextualcomputations upon a large class of structures. With the addition of adistributive law, closed comonads may be composed into spaces ofarbitrary dimensionality while preserving the performance guaran-tees of this new fixed point.

    From these elements, we construct a small embedded domain-specific language to elegantly express and evaluate multidimensionalspreadsheet-like recurrences for a variety of cellular automata.

    Categories and Subject Descriptors D.1.1 [Programming Tech-niques]: Applicative (Functional) Programming; F.3.3 [Studies ofProgram Constructs]: Functional Constructs; F.4.1 [MathematicalLogic and Formal Languages]: Modal Logic

    Keywords Haskell, closed comonads, modal logic, spreadsheets

    1. Introduction

    In 2006, Dan Piponi wrote a blog post, From Lobs Theorem toSpreadsheet Evaluation, [18] the result of which has become acurious piece of folklore in the Haskell community. He writes thatone way to write code is to pick a theorem, find the corresponding

    The work in this paper was performed in part at Brandeis University, USA.

    type, and find a function of that type. As an exercise in this style ofCurry-Howard spelunking, he picks Lobs theorem from the modallogic of provability and attempts to derive a Haskell program towhich it corresponds.

    Several years later, I came across his loebfunction myself andwas fascinated equally by its claimed Curry-Howard connection asby its almost perverse reliance on laziness (to borrow a phrasefrom Done [5]). As I explored loebs world, however, somethingincreasingly seemed to be amiss. Though the term Piponi constructshas a type which mirrors the statement of Lobs theorem, the

    program inhabiting that type (that is, the proof witnessing thetheorem) is built from a collection of pieces which dont necessarilycorrespond to the available and necessary logical components usedto prove Lobs theorem in the land of Howard.

    Piponi is clear that he isnt sure if his loebterm accurately wit-nesses Lobs theorem. I take his initial exploration as an opportunityto embark on a quest for a closer translation of Lobs theorem, fromthe foreign languages of modal logic to the familiar tongue of myhomeland: Haskell. This journey will lead us to find a new efficientcomonadic fixed point which inhabits a more accurate computa-tional translation of Lobs theorem (211). When combined withthe machinery of comonadic composition, well find that this fixedpoint enables us to concisely express spreadsheet-like recurrences(13) which can be generalized to apply to an n-dimensional in-finite Cartesian grid (1415). Using these pieces, we can build a

    flexible embedded language for concisely describing such multi-dimensional recurrences (1618) such as the Fibonacci sequence,Pascals triangle, and Conways game of life (19).

    2. Modal Logic, Squished to Fit in a Small Box

    The first step of the journey is to learn some of the language wereattempting to translate: modal logic. Modal logics extend ordinaryclassical or intuitionistic logic with an additional operator, , whichbehaves according to certain extra axioms. The choice of theseaxioms permits the definition of many different modal systems.

    Martin Hugo Lobs eponymous theorem is the reason this paperexists at all. In the language of modal logic, it reads:

    (P P) P

    If we read as is provable, this statement tells us that, for somestatementP, if its provable that Ps provability impliesPs truth,thenP itself is provable.

    Conventionally, Lobs theorem is taken as an additional axiomin some modal logic [22], but in some logics which permit amodal fixed pointoperation, it may be derived rather than takenas axiomatic [14]. In particular, Lobs theorem is provable in theK4 system of modal logic extended with modal fixed points [15].Well return to this proof later. First, we need to understand Piponisderivation for a Lob-inspired function: its fun and useful, but further,understanding what gets lost in translation brings us closer to a morefaithful translation of the theorem into Haskell.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    2/12

    3. A Bridge Built on a Functor

    To carry Lobs theorem across the river between logic and computa-tion, we first need to build a bridge: an interpretation of the theoremas a type. If we read implication as Haskells function arrow, thepropositional variable P must necessarily translate to a type variableof kind and the modal operator must translate to a type of kind . Thus, the type signature for Lobs computational equivalent

    must take the form:loeb:: ( f) f (f aa) f a

    There is an unspecified constraint( f)in this signature becausewere looking for something of maximal generality, so we wantto leavef anda polymorphic but then we need to choose someconstraint if we want any computational traction to build such aterm.

    Piponi fills the unknown constraint by specifying that f be aFunctor. Under this assumption, he constructs the loebfunction.1

    loeb::Functorf f (f aa) f aloebffs=fix (fa fmap($ fa)ffs)

    Its easiest to get an intuition for this function when f is taken tobe a container-likeFunctor. In this case,loebtakes a container

    of functions, each from a whole container ofas to a single a, andreturns a container ofas where each element is the result of thecorresponding function from the input container applied to the wholeresultant container. More succinctly: loeb finds the unique fixedpoint (if it exists) of a system of recurrences which refer to eachothers solutions by index within the whole set of solutions. Evenmore succinctly:loeblooks like evaluating a spreadsheet.

    Instantiating f as[ ], we find:

    loeb[ length, (!! 0), xx!! 0 +x!! 1] [ 3, 3, 6]

    As expected: the first element is the length of the whole list (whichmay be computed without forcing any of its elements); the secondelement is equal to the first element; the last element is equal to thesum of the first two. Not every such input has a unique fixed point,though. For instance:

    loeb[ length, sum] [ 2,]We cannot compute the second element because it is defined interms of itself, strictly. Any part ofloebs input which requires anelement at its own position to be strictly evaluated will return .More generally, any elements which participate in such a cycle ofany length will return when evaluated with loeb. This is necessary,as no unique solution exists to such a recurrence.

    Instantiatingloebwith other functors yields other similar fixedpoints. In particular, whenf is()r, the reader functor,loebisequivalent to a flipped version of the ordinary fixed point function:

    loeb() r:: (r(ra) a) r aloeb() r fix flip

    (See Appendix A for elaboration.)

    4. Is the Box Strong Enough?

    Despite its intriguing functionality, the claim that loebembodiesa constructive interpretation of Lobs theorem now relativelywidespread doesnt hold up to more detailed scrutiny. In particular,the implementation ofloebusesfimplicitly as a strong functor, and emphatically is not one.

    In order to see why thats a problem, lets take a closer lookat the rules for the use of. The K4 system of modal logic, as

    1For pedagogical reasons, we rephrase this usingan explicit fixpoint; Piponis

    equivalent original is:loebffs = fawherefa= fmap($ fa) ffs .$.

    mentioned earlier, allows us to prove Lobs theorem once we add toitmodalfixed points. This ought to ring some Curry-Howard bells,as were looking for a typeclass to represent which, in conjunctionwithcomputationalfixed points, will give us Lobs program. Theaxioms for the K4 system are:

    A A axiom(4)

    (A B)A

    B distribution axiom

    Informally, axiom (4) means that ifA is provable with no assump-tions then its provably provable, and the distribution axiom tells usthat were allowed to usemodus ponensinside of the operator.

    Additionally, all modal logics have the necessitation rule ofinference that A lets us conclude that A[8]. An importantthing to notice about this rule is that it doesntallow us to deriveA A, though it might seem like it. Significantly, there are noassumptions to the left of the turnstile in A, so we can lift A intothe modality only if we can prove it under no assumptions. If wetry to derive A A, we might use the variable introduction(axiom) rule to getA A, but then get stuck, because we donthave the empty context required by the necessitation rule to then liftAinto the .

    Nevertheless, the necessitation and distribution axioms are suf-

    ficient to show that is a functor: if we have some theorem A B, we can lift it to (A B) by the necessitationrule, and then distribute the implication to get A B. Thus,in modal logic, if we have A B then we have A B,and so whatever our translation of, it should beat leastaFunctor.

    That being said,loebis secretly using its Functoras more thana mere functor it uses it as a strongfunctor. In category theory, afunctorFis strong with respect to some product operation ()ifwe can define the natural transformation F(a) b F(ab)[12].In Haskell,everyFunctoris strong because we can form closuresover variables in scope to embed them in the function argument tofmap. As such, we may flex our functorial muscles any time weplease:2, 3

    flex::Functorf f ab f (a, b)flexfa b = fmap(, b)fa

    We can rephraseloebin a partially point-free style to elucidatehow it depends upon functorial strength:

    loeb::Functorf f (f a a) f aloebffsfix (fmap(uncurry ($)) flexffs)

    (See Appendix B for elaboration.)While the loeb function needs to flex, K4s modality does

    not have the strength to match it. Were not permitted to sneak anarbitrary unboxed value into an existing box and nor shouldwe be. Given a single thing in a box, A, and a weakening law,(A B) B, functorial strength lets us fill the withwhatever we please and thus with functorial strength is nolonger a modality of provability, for it now proves everything whichis true. In Haskell, this operation constitutes filling an a-filled

    functor with an arbitrary other value:fill::Functorf f ab f bfillfa= fmap snd flexfa

    You might knowfill by another name: Data.Functors($>).

    2This definition offlexis curried, but since the product were interested in is(, ),uncurry flexmatches the categorical presentation exactly. We presentit curried as this aids in the idiomatic presentation of later material.

    3In the definition offlex, we make use of GHCs TupleSectionssyntacticextension to clarify the presentation. In this notation, (a, ) b (a, b)and(, b) a(a, b).

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    3/12

    While making a true constructive interpretation of Lobs theo-rem in Haskell, we have to practice a kind of asceticism: thoughfunctorial strength is constantly available to us, we must take careto eschew it from our terms. To avoid using strength, we need tomake sure that every argument to fmapis a closed term. This re-striction is equivalent to the necessitation rules stipulation that itsargument be provable under no assumptions. Haskells type systemcant easily enforce such a limitation; we must bear that burden of

    proof ourselves.4

    Piponi chooses to translate to Functor as an initial guess.But what should become in Haskell? he asks. Well defer thatdecision until later and assume as little as possible, he decides,assigning the unassumingFunctortypeclass to the modal . GiventhatFunctor isnt expressive enough to keep us from needing itsforbidden strength, a closer translation of the theorem requires us tofind a different typeclass for one with a little more oomph.

    5. Contextual Computations, Considered

    Recall thatloebcomputes the solution to a system of recurrenceswhere each part of the system can refer to the solution to the wholesystem (e.g. each element of the list can be lazily defined in terms ofthe whole list). Specifically, individual functions f a areceive

    vialoeba view upon the solved structure f awhich is the samefor each such viewing function.A structure often described as capturing computations in some

    context is the comonad, the monads less ostentatious dual. InHaskell, we can define comonads as a typeclass:

    classFunctorwComonadw whereextract ::w aa -- dual toreturnduplicate::w aw (w a) -- dual tojoinextend :: (w a b ) w a w b -- dual to(==)and joinmay be defined in termsof each other and fmap; the same is true of their duals, extendandduplicate. In Control.Comonad, we have the following default

    definitions, so defining aComonadrequires specifying onlyextractand one ofduplicateor extend:

    duplicate= extend idextendf =fmapf duplicate

    4The typing rules for Cloud Haskells staticspecial form depend on whethera term is closed or not only certain sorts of closed terms may be serialized

    over the network but these rules are specific to this particular feature andcannot presently be used for other purposes [6].

    5These laws also show us that just as a monad is a monoid in the category of

    endofunctors, a comonad is a comonoid in the category of endofunctors:(1) and (2) are left and right identities, and (3) is co-associativity.

    6. A Curious Comonadic Connection

    From now, lets call the hypothetical computational Lobs theoremlfix (for Lob-fix). Well only use the already-popularized loeb torefer to the function due to Piponi.

    Comonads seem like a likely place to search for lfix, not onlybecause their intuition in terms of contextual computations evokesthe behavior ofloeb, but also because the type of the comonadic

    duplicatematches axiom (4) from K4 modal logic.

    P P axiom (4) = duplicate :: w a w(w a)

    Before going on, wed do well to notice that the operations fromComonadare not a perfect match for K4 necessity. In particular,Comonadhas nothing to model the distribution axiom, and extracthas a type which does not correspond to a derivable theorem in K4.Well reconcile these discrepancies shortly in 10.

    That being said, if we browseControl.Comonad, we can findtwo fixed points with eerily similar types to the object of our quest.

    lfix :: ( w) w(w a a) w a

    cfix ::Comonadw (w a a) w a

    wfix::Comonadw w(w a a) a

    The first of these,cfix, comes from Dominic Orchard [17].

    cfix::Comonadw(w aa) w acfixf =extendf (cfixf)

    Its close to the Lob signature, but not close enough: it doesnt takeas input a wof anything; it starts with a naked function, and theresno easy way to wrangle our way past that.

    The second,wfix, comes from David Menendez [16].

    wfix::Comonadww (w aa) awfixf =extractf (extend wfixf)

    It, likecfix, is missing awsomewhere as compared with the type oflfix, but its missing a won the type of itsresult and we can workwith that. Specifically, usingextend:: (w ab ) w aw b,we can define:

    possibility::Comonadww (w aa) w apossibility= extend wfix

    In order to test out this possibility, we first need to build acomonad with which to experiment.

    7. Taping Up the Box

    In the original discussion ofloeb, the most intuitive and instructiveexample was that of the list functor. This exact example wont workfor lfix: lists dont form a comonad because theres no way to extractfrom the empty list.

    Non-empty lists do form a comonad where extract = headandduplicate= init tails, but this type is too limiting. Becauseextendf =fmapf duplicate, the context seen by any extendedfunction in the non-empty-list comonad only contains a rightwards

    view of the structure. This means that all references made inour computation would have to point rightwards a step backin expressiveness from Piponisloeb, where references can pointanywhere in the inputFunctor.

    From Gerard Huet comes the zipperstructure, a family of datatypes each of which represent a single focus element and itscontext within some (co)recursive algebraic data type [9]. Not bycoincidence, every zipper induces a comonad [1]. A systematic wayof giving a comonadic interface to a zipper is to let extractyield thevalue currently in focus and let duplicatediagonalize the zipper,such that each element of the duplicated result is equal to the viewfrom its position of the whole of the original zipper.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    4/12

    This construction is justified by the second comonad law:fmap extract duplicate id. When we diagonalize a zipper,each inner-zipper element of the duplicated zipper is focused on theelement which the original zipper held at that position. Thus, whenwefmap extract, we get back something identical to the original.

    A particular zipper of interest is the list zipper(sometimes knownas the pointed list), which is composed of a focus and a contextwithin a list that is, a pair of lists representing the elements to theleft and right of the focus.

    Though list zippers are genuine comonads with total comonadicoperations, there are other desirable functions which are impossibleto define for them in a total manner. The possible finitude of thecontext lists means that we have to explicitly handle the edge casewhere weve reached the edge of the context.

    To be lazy, in both senses of the word, we can eliminate edgesentirely by placing ourselves amidst an endless space, replacing thepossibly-finite lists in the zippers context with definitely-infinitestreams. TheStreamdatatype is isomorphic to a list without thenilconstructor, thus making its infinite extent (if we ignore ) certain.6

    dataStreama= Consa(Streama)

    By crossing twoStreams and a focus element, we get a zipperinto a both-ways-infinite stream. Well call this datatype a Tapeafter the bidirectionally infinite tape of a Turing machine.7

    dataTapea=Tape{ viewL:: Streama, focus ::a, viewR::Streama} derivingFunctor

    We can move left and right in theTapeby grabbing onto an elementfrom the direction we want to move and pulling ourselves toward it,like climbing a rope.

    moveL,moveR::TapeaTapea

    moveL(Tape(Consl ls)c rs) =Tapels l(Consc rs)moveR(Tapels c(Consr rs)) = Tape(Consc ls)r rs

    Notice thatmoveLand moveRare total, in contrast to their equiva-lents on pointed finite lists.

    Tape forms a Comonad, whose instance we can define usingthe functionality outlined above, as well as an iteration function forbuildingTapes.

    iterate:: (a a) (a a) aTapeaiterateprev next seed=

    Tape(Stream.iterateprev(prev seed))seed

    (Stream.iteratenext(next seed))

    instanceComonad Tape whereextract =focusduplicate= iterate moveL moveR

    As with other comonads derived from zippers, duplicatefor Tapes

    is a kind of diagonalization.

    8. Takingpossibilityfor a Test Drive

    With our newly mintedTapecomonad in hand, its time to kick thetires on this newpossibility. To start, lets first try something reallysimple: counting to 10000.

    6This datatype is defined in the library Data.Stream.7For brevity, we use the GHC extension DeriveFunctor to derive the

    canonicalFunctorinstance forTape.

    tenKilo= print Stream.take10000 viewR possibility$Tape(Stream.repeat (const0)) -- zero left of origin

    (const0) -- zero at origin(Stream.repeat -- right of origin:

    (succ extract moveL)) --1 + leftward value

    Notice that the pattern of recursion in the definition of this Tape

    would be impossible using the non-empty-list comonad, as elementsextending rightward look to the left to determine their values.Enough talk lets run it!. . . . . . . . .

    . . . . . . . . . Unfortunately, that sets our test drive off to quite the slow

    start. On my machine,8 this program takes a full 13 seconds to printthe number 10000. To understand better how abysmally slow that is,lets compare it to a nave list-based version of what wed hope isroughly the same program:

    listTenKilo = print$ Prelude.take10000 [1 . . ]

    This program takes almost no time at all as well it should! TheTape-based counting program is slower than the list-based one by afactor of around 650. Your results may vary a bit of course, but notdramatically enough to call this kind of performance acceptable forall but the most minuscule applications.

    This empirical sluggishness was how I first discovered the inade-quacy ofpossibilityfor efficient computation, but we can rigorously

    justify why it necessarily must be so slow and mathematically char-acterize just how slow that is.

    9. LazinesssansSharing =Molasses in January

    Recall the definitions ofwfixand possibility:

    wfix::Comonadww (w aa) awfixw=extractw(extend wfixw)

    possibility::Comonadww (w aa) w apossibility= extend wfix

    Rephrasingwfix in terms of an explicit fixed-point and subse-quently inlining the definitions offixand extend, we see that

    wfix wf wherewf =wextractw(fmapwf (duplicatew))

    In this version of the definition, its more clear that wfix does notshare the eventual fixed point it computes in recursive calls to itself.The only sharing is ofwfixitself as a function.

    In practical terms, this means that when evaluatingpossibility,every time a particular function w a acontained in the inputw (w a a) makes a reference to another part of the resultstructure w a, the entire part of the result demanded by thatfunction must be recomputed. In the counting benchmark above,this translates into an extra linear factor of time complexity in what

    should be a linear algorithm.Worse still, in recurrences with a branching factor higher thanone, this lack of sharing translates not merely into a linear cost, butinto an exponential one. For example, each number in the Fibonaccisequence depends upon its two predecessors, so the followingprogram runs in time exponential to the size of its output.9

    8Compiled using GHC 7.8.3 with -O2 optimization, run on OS X 10.10 with

    a 2 GHz quad-core processor.9The following code uses do-notation with the reader monad () r to

    express the function summing the two elements left of a Tapes focus.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    5/12

    slowFibs= print Stream.taken viewR possibility$Tape(Stream.repeat (const0))

    (const1)(Stream.repeat $ do

    aextract moveLb extract moveL moveLreturn$ a+ b)

    wheren= 30 -- increase this number at your own peril

    This is just as exponentially slow as a nave Fibonacci calculationin terms of explicit recursion with no memoization.

    10. Finding Some Closure

    It would be disappointing if the intractably slow possibilityweretruly the most efficient computational translation of Lobs theorem.In order to do better, lets first take a step back to revisit the guidingCurry-Howard intuition which brought us here.

    Since Lobs theorem is provable in K4 modal logic augmentedwith modal fixed points, we tried to find a typeclass which mirroredK4s axioms, which we could combine with the fix combinator toderive ourlfix. We identifiedComonadsduplicatewith axiom (4)

    of K4 logic and derived a fixed point which used this, the comonadicextract, and the functoriality (without needing strength) of. Asmentioned before, theres something fishy about this construction.

    Firstly, K4 modal logic has nothing which corresponds to thecomonadicextract:: Comonad w w a a. When we addto K4 the axiom A A (usually called (T) in the literature) towhich extract corresponds, we get a different logic; namely, S4modal logic. Unfortunately, axiom (T) when combined with L obstheorem leads to inconsistency: necessitation upon (T) gives usA.(A A), then Lobs theorem gives us A.A, and finally,applying (T) yields A.A: a logical inconsistency.

    As a result, no good translation of Lobs theorem can useextractor anything derived from it, as a proof using inconsistent axiomsis no proof at all. Notably, the wfixwe used in defining possibilitymust be derived in part from extract. If its slow performance didntalready dissuade us, this realization certainly disqualifies possibilityfrom our search.10

    The second mismatch between K4 andComonadis the lattersabsence of something akin to the distribution axiom: Comonadgives us no equivalent of(A B) A B.

    The distribution axiom should look familiar to Haskellers. Squintslightly, and it looks identical to the signature of the Applicativesplat operator().11 Compare:

    () ::Applicativef f(a b) f a f b

    distribution axiom: (A B) A B

    At first glance, this seems quite promising we might hastilyconclude that the constraint matching the modal is that of anApplicative Comonad. But while () is all well and good, theother method ofApplicative ruins the deal: pure has exactly thetype we cant allow into the :Applicativef a f a, whichcorresponds to the implicationA A, which weve been tryingto escape from the start!

    Hope need not be lost: another algebraic structure fits the billperfectly: the closed comonad. In Haskell, the ComonadApply

    10What we really need is a semi-comonad that is, a comonad withoutextract. The programming convenience ofextract, however, makes itworthwhile to stick withComonad, but with a firm resolution not to useextractin deriving ourlfix. We discuss this more comprehensively in12.

    11What is typeset here as()is spelled in ASCIIHaskell as ().

    typeclass models the closed comonad structure (at least up to values

    containing ).12

    classComonadwComonadApplyw where( ) ::w (ab ) w aw b

    ComonadApplylacks a unit likeApplicativespure, thus freeingus from unwanted pointedness.

    A brief detour into abstract nonsense: the structure impliedbyComonadApply is, to quote the documentation, a strong laxsymmetric semi-monoidal comonad on the categoryHaskof Haskelltypes, [10]. A monoidal comonad is equivalent to a closed comonadin a Cartesian-closed category (such as that of Haskell types), andthis structure is exactly whats necessary to categorically modelintuitionistic S4 modal logic [3][2]. If we avoid using extract, wefind ourselves where we wanted to be: in the world of intuitionisticK4 modal logic.

    11. Putting the Pieces Together

    Now that weve identified ComonadApply as a slightly-overpoweredmatch for the modality, its time to put all the pieces together.Because Functor is a superclass ofComonad, which itself is asuperclass ofComonadApply, we have all the methods of these

    three classes at our disposal. Somehow, we need to assemble ourfunctionlfix from a combination of these specific parts:13

    fix :: (a a) afmap ::Functorw(ab ) w aw bduplicate::Comonadww aw (w a)( ) ::ComonadApplyww (ab ) w aw b

    One promising place to start is with fix. We know from experiencewithwfixthat well want to share the result oflfixin some recursiveposition, or well pay for it asymptotically later. If we set up ourfunction as below, we can guarantee that we do so.

    lfix::ComonadApplyww (w aa) w alfixf =fix

    GHC will gladly infer for us that the hole ( ) above has type

    w a w a.14

    If we didnt have typed holes, we could certainlysee why for ourselves by manual type inference:fix, specializedfrom(a a) ato return a result of type w a, must take asinput something of type w a w a. We can elide such chains ofreasoning below I prefer to drive my type inference on automatic.

    We might guess that, like in cfix, we have toduplicateour inputin each recursive call:

    lfixf =fix ( duplicate)

    Further, since we havent yet usedfin the right hand side of ourdefinition, a good guess for its location is as an argument to theunknown( ).

    lfixf =fix ( f duplicate)

    This new hole is of the type w (w a a) w (w a) w a,

    a specialization of the type of the ComonadApplyoperation,( ).Plugging it in finally yields the lfixfor which weve been searching.

    lfix::ComonadApplyww (w aa) w a

    lfixf =fix

    (f ) duplicate

    12What is typeset here as( )is spelled in ASCIIHaskell as ().13Note the conspicuous absence of extract, as our faithfulness to K4

    prevents us from using it in our translation of the proof.14This typed hole inference is present in GHC versions 7.8 and greater.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    6/12

    12. Consulting a Road Map

    The term lfix checks many boxes on the list of criteria for acomputational interpretation of Lobs theorem. Comparing it againsta genuine proof of the theorem in its native habitat due to Mendelson[15] shows that lfix makes use of a nearly isomorphic set ofprerequisites:duplicate(axiom 4),( )(distribution axiom), andfix, which as used here roughly corresponds to the role of modal

    fixed points in the proof.15

    Mendelson doesnt prove Lobs theorem in the exact form weveseen; rather, he proves(P P) P. This statement, though,implies the truth of the version weve seen: to raise everything upa box, we need to apply the necessitation and distribution rules toderive our version, (P P) P.

    A quick summary of modal fixed points: If a logic has modalfixed points, that means we can take any formula with one free vari-ableF(x)and find some other formulasuch thatF() . The particular modal fixed point required by Mendelsons proofis of the form(L C) L, for some fixed C.

    There is a slight discrepancy between modal fixed points andHaskells value-level fixed points. The existence of a modal fixedpoint is an assumption about recursive propositions and thus, cor-responds to the existence of a certain kind of recursive type. Bycontrast, the Haskell termfix expresses recursion at the value level,not the type level. This mismatch is, however, only superficial. ByCurrys fixed point theorem, we know that value-level general re-cursion is derivable from the existence of recursive types. Similarly,Mendelsons proof makes use of a restricted family of recursivepropositions (propositions with modal fixed points) to give a re-stricted kind of modal recursion. By using Haskells fix ratherthan deriving analogous functionality from a type-level fixed point,weve used a mild sleight of hand to streamline this step in the proof.Consider the following term (and its type), which when applied to(f )yields ourlfix:

    g fix (g duplicate)::Comonadw(w (w a) w a) w a

    Because our lfix is lifted up by a box from Mendelsons proof,the fixed point we end up taking matches the form L (L L), which lines up with the type of the term above.

    In Haskell, not every expressible (modal) fixed point has a(co)terminating solution. Just as in the case ofloeb, its possible touselfixto construct cyclic references, and just as in the case ofloeb,any cycles result in . This is exactly as we would expect for aconstructive proof of Lobs theorem: if the fixed point isnt uniquelydefined, the premises of the proof (which include the existence of afixed point) are bogus, and thus we can prove falsity ().

    Another way of seeing this: the hypothesis of Lobs theorem(P P) is effectively an assumption that axiom (T) holdsnot in general, but just for this one proposition P. Indeed, theinputs tolfix which yield fully productive results are precisely thosefor which we can extract a non-bottom result from any location(i.e. those for which axiom (T) always locally holds). Any way tointroduce a cycle to such a recurrence must involve extractor some

    specialization of it without it, functions within the recurrence canonly refer to properties of the whole container (such as length)and never to specific other elements of the result. Just as we can(mis)useFunctoras a strong functor (noted in 4), we can (mis)useComonadsextractto introduce non-termination. In both cases, thetype system does not prevent us from stepping outside the lawsweve given ourselves; its our responsibility to ensure some kindsof safety and if we do, lfixs logical analogy does hold. Vitally, the

    15Notably, we dont need to use the necessitation rule in this proof,which means that we can get away with a semi-monoidal comonad alaComonadApply .

    proof witnessed bylfix is itself consistent: it does not use extract.Inconsistency is only (potentially) introduced when the programmerchooses to externally combinelfixand extract.

    13. A Zippier Fix

    As a result of dissecting Mendelsons proof, weve much greaterconfidence this time around in our candidate term lfix and its closer

    fidelity to the modality. In order to try it out on the previous example,we first need to give an instance ofComonadApplyfor theTapecomonad. But what should that instance be?

    The laws which come with the ComonadApplyclass, those of asymmetric semi-monoidal comonad, are as follows:

    () $ u v wu (v w) (1)

    extract(p q) extractp (extractq) (2)

    duplicate(p q) ( ) $ duplicatep duplicateq (3)

    In the above, we use the infixfmapoperator:($ ) =fmap.16$

    Of particular interest is the third law, which enforces a certainstructure on the( ) operation. Specifically, the third law is theembodiment forComonadApplyof the symmetricin symmetricsemi-monoidal comonad. It enforces a distribution law which can

    only be upheld if( )is idempotent with respect to cardinality andshape: if someris the same shape as p q, thenr p qmustbe the same shape as well [19]. For instance, the implementationofApplicatives()for lists a computation with the shape of aCartesian product and thus an inherent asymmetry would fail thethirdComonadApplylaw if we used it to implement ( )for the(non-empty) list comonad.17

    We functional programmers have a word for operations likethis: ( ) must have the structure of a zip. Its for this reasonthat Uustalu and Vene define a ComonadZip class, deriving theequivalent of ComonadApply from its singular method czip ::(ComonadZip w) w a w b w (a, b) [20]. Theczip and ( ) operations may be defined only in terms of oneanother and fmap their two respective classes are isomorphic.Instances ofComonadApply generally have fewer possible law-

    abiding implementations than do those ofApplicativebecause theyare thus constrained to be zippy.This intuition givesus thetools we need to write a properinstance

    ofComonadApplyfirst forStreams . . . 18

    instanceComonadApply Streamwhere

    Consx xs Consx xs =

    Cons(x x) (xs xs)

    ...and then for Tapes, relying on the Stream instance we justdefined to properly zip the componentStreams of theTape.

    instanceComonadApply Tape where

    Tapels c rs Tapels c rs =

    Tape(ls ls) (c c) (rs rs

    )

    With these instances in hand (or at least, in scope), we can run

    the trivial count-to-10000 benchmark again. This time, it runs inlinear time with only a small constant overhead beyond the navelist-based version.

    In this new world, recurrences with a higher branching factorthan one no longer exhibit an exponential slowdown, instead runningquickly by exploiting the sharinglfix guarantees on container-like

    16What is typeset here as($ )$is spelled in ASCIIHaskell as ().$17For a type instantiating bothApplicativeand ComonadApply, the equiv-

    alence( ) ()should always hold [10].18We rely here on a Comonadinstance forStreams analogous to that for

    non-empty lists given in7:extract= headandduplicate= tails.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    7/12

    functors. If we rewrite the Fibonacci example mentioned earlier touselfixinstead ofpossibility, it becomes extremely quick on mymachine, it computes the 10000th element of the sequence in 0.75seconds.

    A memoizing Fibonacci sequence can be computed by a fixedpoint over a list:

    listFibs=

    fix$ fibs0 : 1 :zipWith(+) fibs(tailfibs)

    The Fibonacci sequence in terms oflfix is extensionallyequiv-alent to the well-known one-liner above; moreover, they are alsoasymptoticallyequivalent and in practice, the constant factor over-

    head for thelfix-derived version is relatively small.19

    To summarize: the zip operation enabled by ComonadApplyis the source oflfixs computational zippiness. By allowing theeventual future value of the comonadic result w ato be shared byevery recursive reference,lfixensures that every element of its resultis computed at most once.20

    14. Building a Nest in Deep Space

    The only comonad weve examined in depth so far is the one-dimensionalTapezipper, but there is a whole world of comonadsout there. Piponis original post is titled, From Lobs Theoremto Spreadsheet Evaluation, and as weve seen, loeb on the listfunctor looks just like evaluating a one-dimensional spreadsheetwith absolute references. Likewise, lfixon theTapecomonad looksjust like evaluating a one-dimensional spreadsheet with relativereferences.

    Its worth emphasizing that loeb and lfix computedifferent things.The proof embodied bylfix contains as its computational contenta totally distinct pattern of (co)recursion from the simpler one ofloeb. Their results will coincide only on very particular inputs.21

    So far, all weve seen are analogues to one-dimensional spread-sheets but spreadsheets traditionally havetwo dimensions. Wecould build a two-dimensionalTapeto represent two-dimensionalspreadsheets nest a Tape within another Tape and wed have madea two-dimensional space to explore but this is an unsatisfactory

    special case of a more general pattern. As Willem van der Poelapocryphally put it, wed do well to heed the zero, one, infinityrule. Why make a special case for two dimensions when we can talkaboutn N dimensions?

    TheComposetype22 represents the composition of two typesfandg, each of kind :

    newtypeComposef g a=Compose{ getCompose::f (g a)}

    We can generalizeComposeusing a GADT indexed by a type-levelsnoc-list of the composed types it wraps:23

    19This constant factor is around 1.3x in my own testing.20Notably, lfix exhibits this memoization only on structural comonads.

    Haskell does not automatically memoize the results of functions but doesmemoize thunks computing elements of data structures. As a result, thecomonad newtype ZTape a = ZTape (Integer a), thoughisomorphic to Tape, does not get the full benefit oflfixs performanceboost, whileTapedoes.

    21In particular,lfixandloebcoincide over inputs that contain no functionsdepending on locally contextual information.

    22This type is canonically located in Data.Functor.Compose.23Rather than letting Flatand Nest inhabit a new kind (via a lifted sum

    type), we let them be uninhabited types of kind so we can alias thetype and value constructor names. If GHC had kind-declarations without

    associated types, we could have increased kind-safety as well as this kindof punning between the type and value levels.

    dataFlat(only:: )dataNest(outsides:: ) (innermost:: )

    dataNestedfs awhereFlat::f aNested(Flatf) aNest::Nestedfs(f a) Nested(Nestfs f)a

    To find an intuition for howNestedoperates, observe how the type

    changes as we add Nestconstructors:[Just True] :: [ Maybe Bool]

    Flat[ Just True] ::Nested (Flat[ ]) (Maybe Bool)Nest(Flat[ Just True]) ::Nested(Nest(Flat[ ]) Maybe)Bool

    The only way to initially make some type Nestedis to first call theFlatconstructor on it. Subsequently, further outer layers of structuremay be unwrapped from the type using theNestconstructor, whichmoves a layer of type application from the second parameter to thesnoc-list inNesteds first parameter.

    In this way, while Compose canonically represents the com-position of types only up to associativity (for any f, g, and h,Compose f (Compose g h) =Compose (Compose f g) h), theNestedtype gives us a single canonical representation by removingthe choice of left-associativity.

    Its trivial to define a getComposeaccessor for theComposetype, but in order to provide the same functionality forNested, weneed to use a closed type family to describe the type resulting fromunwrapping one ofNesteds constructors.

    type family UnNestx whereUnNest(Nested(Flatf) a) = f aUnNest(Nested(Nestfs f)a) = Nestedfs(f a)

    unNest::Nestedfs a UnNest(Nestedfs a)unNest(Flat x) = xunNest(Nestx) = x

    Having wrought this new type, we can put it to use by definingonce and for all how to evaluate an arbitrarily deep stack ofcomonadic types as a higher-dimensional spreadsheet.

    15. Introducing Inductive Instances

    TheNested type enables us to encapsulate inductive patterns intypeclass definitions for composed types. By giving two instancesfor a given typeclass the base case for Nested (Flat f), andthe inductive case for Nested (Nest fs f) we can instantiate atypeclass forall Nestedtypes, no matter how deeply composed.

    The general design pattern illustrated here is a powerful tech-nique to write pseudo-dependently-typed Haskell code: model theinductive structure of some complex family of types in a GADT,and then instantiate typeclass instances which perform ad-hocpolymorphic recursion on that datatype.

    For Nesteds Functor instance, the base case Nested(Flatf) re-lies uponfto be a Functor; the inductive case,Nested(Nestfs f)relies onfas well asNestedfsto beFunctors.

    instanceFunctorf Functor(Nested(Flatf))wherefmapg=Flat fmapg unNest

    instance(Functor f ,Functor (Nestedfs))Functor (Nested(Nestfs f))where

    fmapg=Nest fmap(fmapg) unNest

    With that, arbitrarily large compositions ofFunctors may nowthemselves beFunctors as we know to be true.

    TheNestedtype will only give usn-dimensional spreadsheetsif we provide a bit more than Functor, though we also needinductive instances forComonadand ComonadApply.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    8/12

    As with Functor, defining the base-case Comonad instance that for Nested (Flat f) is relatively straightforward; we leanon theComonad instance of the wrapped type, unwrapping andrewrapping as necessary.

    instanceComonadwComonad(Nested(Flatw))whereextract =extract unNestduplicate= fmap Flat Flat

    duplicate unNest

    The inductive case is trickier. To duplicate something of typeNested (Nest fs f) a, we need to create something of typeNested(Nestfs f) (Nested(Nestfs f)a).

    Our first step must be tounNest, as we cant do much withoutlooking inside theNestedthing. This gives us aNested fs (f a).Iff is aComonad, we can now fmap duplicateto duplicatethenewly revealed innerlayer, giving us a Nested fs (f (f a)). If(inductively)Nested fs is a Comonad, we can alsoduplicatetheouterlayer to get aNestedfs(Nestedfs(f (f a))).

    Here is where, with merely our inductive Comonadconstraintsin hand, we get stuck. We need to distribute f overNestedfs, butwe have no way to do so with only the power weve given ourselves.

    In order to compose comonads, we need to add another precon-

    dition to what weve seen so far: distributivity.24

    classFunctorgDistributivegwheredistribute::Functorf f (g a) g(f a)

    Haskellers might recognize this as the almost-dual to the more fa-miliarTraversableclass, wheredistributeis the dual tosequenceA.Both Traversableand Distributiveenable us to define a functionof the form f (g a) g (f a), but a Traversable f constraintmeans we can push an fbeneath an arbitraryApplicative, while aDistributivegconstraint means we can pull agfrom underneathan arbitraryFunctor.

    With only a Comonadconstraint on all the types in a Nested, wecanduplicatethe inner and outer layers as above. To finish derivingthe inductive-case instance for Nested comonads, we need onlydistributeto swap the middle two layers of what have become four,

    and re-wrap the results inNesteds constructors to inject the resultback into theNestedtype:

    instance(Comonadw,Comonad(Nestedws),Functor (Nested(Nestws w)),Distributivew)

    Comonad(Nested(Nestws w))where

    extract =extract extract unNestduplicate= fmap Nest Nest

    fmap distribute duplicate fmap duplicate unNest

    After that, its smooth sailing to defineComonadApplyin both

    the base and inductive cases for Nested:instanceComonadApplyw

    ComonadApply(Nested(Flatw))whereFlatg Flatx=Flat(g x)

    instance(ComonadApplyw,ComonadApply(Nestedws),Distributivew)

    ComonadApply(Nested(Nestws w))where

    Nestg Nestx=Nest(( ) $ g x)

    24A version of this class may be found inData.Distributive.

    Along these lines, we can instantiate many other inductive instancesfor Nested types, including Applicative, Alternative, Foldable,Traversable, andDistributive.

    Of course, we cant very well use such instances withoutDistributive instances for the base types we intend to Nest. Toelucidate how todistribute, lets turn again to the Tape.

    Formally, aDistributiveinstance for a functor gwitnesses theproperty that gis a representable functor preserving all limits that is, its isomorphic to () rfor some r [11]. We know thatany Tape a, representing a bidirectionally infinite sequence ofas, is isomorphic to functions of type Integer a(though withpotentially better performance for certain operations). Therefore,Tapes must be Distributive but we havent concluded this in aparticularly constructiveway. How can we build such an instance?

    In order to distribute Tapes, we first need to learn how to unfoldthem. Given the standardunfoldoverStreams . . .

    Stream.unfold:: (c (a, c)) c Streama

    . . . we can build anunfoldfor Tapes:

    unfold :: (c (a, c)) -- leftwards unfolding function(c a) -- function from seed to focus value(c (a, c)) -- rightwards unfolding function

    c -- seedTapea

    unfold prev center next seed=Tape(Stream.unfoldprev seed)

    (center seed)(Stream.unfoldnext seed)

    With thisunfold, we can define adistributefor Tapes.25

    instanceDistributive Tape wheredistribute=

    unfold(fmap(extract moveL) &&&fmap moveL)(fmap extract)(fmap(extract moveR) &&&fmap moveR)

    This definition of distribute unfolds a new Tape outside the

    Functor, eliminating the innerTapewithin it byfmapping move-ment and extraction functions through theFunctor layer. Notably,the shape of the outer Tape we had to create could not possiblydepend on information from the innerTapelocked up inside of thef (Tape a). This is true in general: in order for us to be able todistribute, every possible value of any Distributivefunctor musthave a fixed shape and no extra information to replicate beyond itspayload ofavalues [11].

    16. Asking for Directions in Higher Dimensions

    Although we now have the type language to describe arbitrary-dimensional closed comonads, we dont yet have a good way to talkabout movement within these dimensions. The final pieces to thepuzzle are those we need to refer to relative positions within thesenested spaces.

    Well represent an n-dimensional relative reference as a listof coordinates indexed by its length n using the conventionalconstruction for length-indexed vectors via GADTs.

    dataNat = S Nat| Z

    infixr :::dataVec(n:: Nat) (a:: )where

    Nil ::Vec Za(:::) ::a Vec n aVec(Sn)a

    25We define the fanout operator as f &&&g= x(f x, g x). Thisis a specialization of a more general function from Control.Arrow.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    9/12

    A single relative reference in one dimension is a wrappedInt. . .

    newtypeRef= Ref{ getRef:: Int }

    ...and ann-dimensional relative coordinate is ann-vector of these:

    type Coordn= VecnRef

    We can combine together two different Coords of potentially

    different lengths by adding the corresponding components andletting the remainder of the longer dangle off the end. This preservesthe understanding that ann-dimensional vector can be consideredas anm-dimensional vector (n m) where the last (m n) of itscomponents are zero.

    In order to define this heterogeneous vector addition function(&), we need to compute the length of its resulting vector in termsof a type-level maximum operation over natural numbers.

    type familyMax n mwhereMax(Sn) (Sm) =S (Maxn m)Max n Z =nMax Z m =m

    (&) ::CoordnCoordmCoord(Maxn m)(Refq::: qs) & (Refr::: rs) = Ref(q+ r) ::: (qs&rs)qs &Nil =qsNil &rs =rs

    The way the zip operation in(&)handles extra elements in thelonger list means that we should consider the first element of aCoordto be the distance in the first dimension. Since we alwayscombine from the front of a vector, adding a dimension constitutestackinganother coordinate onto the end of a Coord.

    Keeping this in mind, we can build convenience functions forconstructing relative references in those dimensions we care about.

    type Sheet1= Nested(Flat Tape)

    rightBy, leftBy::Int Coord(S Z)rightByx =Refx::: NilleftBy =rightBy negate

    type Sheet2= Nested(Nest(Flat Tape)Tape)

    belowBy, aboveBy::Int Coord(S(S Z))belowByx=Ref0 :::Refx::: NilaboveBy =belowBy negate

    type Sheet3= Nested(Nest(Nest(Flat Tape)Tape)Tape)

    outwardBy, inwardBy::Int Coord(S(S(S Z)))outwardByx=Ref0 :::Ref0 :::Refx :::NilinwardBy =outwardBy negate

    ...

    We can continue this pattern ad infinitum(or at least,adsomevery largefinitum), and the pattern could easily be automated viaTemplate Haskell, should we desire.

    We choose here the coordinate convention that the positive

    directions, in increasing order of dimension number, are right,below, and inward; the negative directions are left, above, andoutward. These names are further defined to refer to unit vectors intheir respective directions (that is,right= rightBy1 and so forth).

    An example of using this tiny language: the coordinate specifiedbyright & aboveBy 3 :: Coord (S (S Z))refers to the selfsamerelative position indicated by reading it aloud as English.

    A common technique when designing a domain specific languageis to separate its abstract syntax from its implementation. This isexactly what weve done weve defined how to describerelativepositions in n-space; what we have yet to do is interpret thosecoordinates.

    17. Following Directions in Higher Dimensions

    Moving about in one dimension requires us to either move left orright by the absolute value of a reference, as determined by its sign:

    tapeGo::Ref TapeaTapeatapeGo(Refr) =

    foldr()id $replicate(absr) (if r< 0thenmoveLelsemoveR)

    Going somewhere based on an n-dimensional coordinate meanstaking that coordinate and some Tape of at least that number ofdimensions, and moving around in it appropriately.

    classGo n twherego::Coordnt at a

    We can go nowhere no matter where we are:

    instanceGo Z(Nestedts)wherego = const id

    Going somewhere in a one-dimensional Tapereduces to calling theunderlyingtapeGofunction:

    instanceGo (S Z) (Nested(Flat Tape))wherego(r ::: ) (Flatt) =Flat(tapeGo r t)

    As usual, its the inductive case which requires more consideration.If we can move in n1dimensions in an(n1)-dimensional Tape,then we can move in n dimensions in ann-dimensionalTape:

    instance(Go n (Nestedts),Functor (Nestedts))Go(Sn) (Nested(NesttsTape))where

    go(r::: rs)t=Nest gors fmap(tapeGo r) unNest$ t

    Notice how this (polymorphically) recursive definition treats thestructure of the nested Tapes: the innermost Tape always corre-sponds to the first dimension, and successive dimensions correspondto Tapes nested outside of it. In each recursive call to go, we unwrapone layer of theNestedtype, revealing another outer layer of thecontained type, to be accessed viafmap(tapeGo r).

    In an analogous manner, we can also use relative coordinates

    to specify how to slice out a section of an n-dimensionally NestedTape, starting at our current coordinates. The only twist is that weneed to use an associated type family to represent the type of theresultantn-nested list.

    tapeTake::RefTapea[ a]tapeTake(Refr)t=

    focust: Stream.take(absr) (view t)whereview=if r< 0thenviewLelseviewR

    classTaken t wheretype ListFromt atake::Coordn t aListFromt a

    instanceTake(S Z) (Nested(Flat Tape))wheretype ListFrom(Nested(Flat Tape))a= [ a]take(r ::: ) (Flatt) = tapeTaker t

    instance(Functor (Nestedts),Take n(Nestedts))Take(Sn) (Nested(Nestts Tape))where

    type ListFrom(Nested(Nest ts Tape))a=ListFrom(Nestedts) [ a]

    take(r::: rs)t=takers fmap(tapeTaker) unNest$ t

    18. New Construction in Higher Dimensions

    To use an analogy to more low-level programming terminology,weve now defined how topeekat Nested Tapes, but we dont yet

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    10/12

    know how topokethem. To modify such structures, we can again usean inductive typeclass construction. The interface we want shouldtake ann-dimensionally nested container of some kind, and insertits contents into a given nestedTapeof the same dimensionality. Inother words:

    classInsertNested ls tswhereinsertNested::Nestedls aNestedts aNestedts a

    Its not fixed from the outset which base types have a reasonablesemantics for insertion into an n-dimensionally nested space. Sothat we can easily add new insertable types, well split out one-dimensional insertion into its own typeclass,InsertBase, and defineInsertNesteds base case in terms ofInsertBase.

    classInsertBasel t whereinsertBase::l at at a

    An instance ofInsertBasefor some type lmeans that we know howto take anl aand insert it into at ato give us a newt a. Its instancefor one-directionally extending types is right-biased by convention.

    instanceInsertBase[ ] Tape whereinsertBase[ ] t=tinsertBase(x: xs) (Tape ls c rs) =

    Tape ls x (Stream.prefixxs(Consc rs))instanceInsertBase Stream Tape where

    insertBase(Consx xs) (Tapels ) = Tape ls x xs

    instanceInsertBase Tape Tape whereinsertBaset =t

    Now were in a better position to define the dimensional inductionnecessary to insertn-nested things inton-nestedTapes. The basecase relies oninsertBase, as expected:

    instanceInsertBasel tInsertNested(Flatl) (Flatt)where

    insertNested(Flatl) (Flatt) =Flat(insertBasel t)

    The trick in the recursive case is to generate a Nested structurefull of functions, each of which knows how to insert the relevant

    elements at their own position into the pre-existing structure there and then to use ( ) to zip together this space of modifying functionswith the space of arguments to which they apply.

    instance(InsertBasel t, InsertNestedls ts,Functor (Nestedls),Functor (Nestedts),ComonadApply(Nestedts))

    InsertNested(Nestls l) (Nestts t)where

    insertNested(Nest l) (Nestt) =Nest$ insertNested(insertBase $ l) (fillt id) t

    Note that although the above instance uses the fill function togenerate a nested structure filled with the identity function, it isnot abusing functorial strength in so doing, as id is a closed term.

    Using some mildly Hasochistic type hackery (ala [13]) wecan take a nested structure which is not yet Nested such as a

    triply-nested list[ [[ Int]] ] and lift it into a Nestedtype such asNested(Nest(Flat[]) []) [Int]. TheasNestedAsfunction has thetype

    asNestedAs::NestedAs x yxy AsNestedAsx y

    where theAsNestedAs x ytype family application describes thetype resulting from wrappingxin as many constructors ofNestedas are wrapped aroundy, and theNestedAstypeclass witnesses thatthis operation is possible. (See Appendix C for a full definition ofthis function.)

    WithasNestedAs, we can define an insertfunction which doesnot require the inserted thing to be already wrapped in a Nested type.

    This automatic lifting requires knowledge of the type into whichwere inserting values, which means that insertand functions basedupon it may require some light type annotation to infer properly.

    insert:: (InsertNestedl t,NestedAsx (Nestedt a),AsNestedAsx (Nestedt a) Nestedl a)

    x Nestedt a Nestedt ainsert l t=insertNested(asNestedAsl t)t

    With that in place, we can define the high level interface to ourspreadsheet library. Borrowing from the nomenclature of spread-sheets, we define two cell accessor functions one to dereferencean individual cell, and another to dereference a Traversablecollec-tion of cells:

    cell:: (Comonadw,Gon w) Coordnw a acell= (extract ) go

    cells:: (Traversable t,Comonadw,Gon w)t(Coordn) w at a

    cells= traverse cell

    We may use thesheetfunction to construct a sheet with a defaultbackground into which some other values have been inserted:

    sheet:: (InsertNestedl ts,ComonadApply(Nestedts),Applicative(Nestedts),NestedAsx (Nestedts a),AsNestedAsx (Nestedts a) Nestedl a)

    axNestedts asheetbackground list= insertlist(purebackground)

    Becausesheethas to invent an entire infinite space of values, weneed to rely on pure from the Applicative class to generate thebackgroundspace into which it may insert its listargument. Thisis not an issue for Tapes, as they are indeed Applicative, where() = ( )andpure= tapeOf,tapeOfbeing the function whichmakes aTapeby repeating a single value. This constraint doesntmean we cant build and manipulate spreadsheets with non-

    Applicativelayers; it merely means we cant as easily manufacturethemex nihilo. Thesheetfunction is purely a pleasing convenience,not a necessity.

    19. Conclusion: Zippy Comonadic Computations

    in Infiniten-Dimensional Boxes

    This journey hastakenus from the general andabstract to the specificand concrete. By transliterating and composing the axioms of the modality, we found a significantly more accurate translation of Lobstheorem into Haskell, which turned out to embody a maximallyefficient fixed point operation over closed comonads. Noticing thatclosed comonads can compose with the addition of a distributive law,we lifted this new fixed-point into spaces composed of arbitrarily

    nested comonads. On top of this framework, we layered typeclassinstances which perform induction over dimensionality to providean interpretation for a small domain-specific language of relativereferences into one family of representable comonads.

    Now, where can we go?

    To start, we can construct the Fibonacci sequence with optimalmemoization, using syntax which looks a lot nicer than before:26

    26In the following examples, we make use of a Numinstance for functionsso thatf +g= a f a+ g a, and likewise for the other arithmeticoperators. These instances may be found in Data.Numeric.Function.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    11/12

    fibonacci::Sheet1 Integerfibonacci= lfix sheet1 $

    repeat $ cell(leftBy2) + cell left

    If we take a peek at it, its as wed expect:

    take(rightBy15) go(leftBy2) $fibonacci[ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

    Moving up into two dimensions, we can define the infinite gridrepresenting Pascals triangle. . . 27

    pascal::Sheet2 Integerpascal= lfix sheet0 $

    repeat 1 : repeat(1 : pascalRow)wherepascalRow=repeat$ cell above+cell left

    . . . which looks like this:

    take(belowBy4 &rightBy4) pascal[ [1, 1, 1, 1, 1 ],

    [1, 2, 3, 4, 5 ],[1, 3, 6, 10, 15],[1, 4, 10, 20, 35],[1, 5, 15, 35, 70]]

    Likefibonacci,pascalevaluates each part of its result at most once.Conways game of life [7] is a nontrivial comonadic computation

    [4]. We can represent a cell in the life universe as either X (dead) orO(alive):

    dataCell= X | O derivingEq

    More interestingly, we define a Universe as a three-dimensionalspace where two axes map to the spatial axes of Conways game,but the third representstime.

    type Universe= Sheet3 Cell

    We can easily parameterize by the rules of the game whichneighbor counts trigger cell birth and death, respectively:

    type Ruleset= ([Int ], [Int])

    To compute the evolution of a game from a two-dimensional listof cells seeding the system, we insert that seed into a blankUniversewhere each cell not part of the seed is defined in terms ofthe action of the rules upon its previous-timestep neighbors. We can

    then evaluate this function space withlfix.28

    life::Ruleset [ [Cell]] Universeliferuleset seed=

    lfix$ insert[ map(map const)seed]blankwhere

    blank=sheet(const X) (repeat tapeOf tapeOf$rule)

    rule place=caseonBoth(neighbors place)ruleset of

    (True, ) O

    ( ,True) cell inwardplaceXneighbors=length filter(O ==) cells borderingbordering=map (inward&) (diag++vert++horz)diag = (&) $ horz vertvert = [ above, below ]horz =map to2D [right, left]to2D= (belowBy0 &)

    27What is typeset here as(: )is spelled in ASCIIHaskell as (), and isdefined to beStreamsConsconstructor in infix form.

    28In the definition oflife, we letonBoth f (x, y) = (f x,f y).

    Conways original game is instantiated by applying the more generallifefunction to his defining rule set:

    conway:: [[ Cell] ] Sheet3 Cellconway= life([3], [2, 3])

    After defining a pretty-printer for Universes (elided here due tospace), we can watch the flight of a glider as it soars off to infinity.

    glider::Sheet3 Cellglider= conway[ [O,O,O],

    [X,X,O ],[X,O,X]]

    printLife glider

    20. Further Work

    I have a strong suspicion thatlfix and the slowpossibilityfrom ear-lier in the paper are extensionally equivalent despite their differingasymptotic characteristics. A precise characterization of the criteria

    for their equivalence would be useful and enlightening.In this paper, I focused on a particular set of zipper topologies:n-dimensional infinite Cartesian grids. Exploring the space ofexpressible computations withlfix on more diverse topologies maylend new insight to a variety of dynamic programming algorithms.

    I suspect that it is impossible to thread dynamic cycle-detectionthroughlfix while preserving its performance characteristics andwithout resorting to unsafe language features. A resolution to thissuspicion likely involves theoretical work on heterogeneous com-positions of monads and comonads. Further, it could be worth ex-ploring how to use unsafe language features to implement dynamiccycle detection inlfixwhile still presenting a pure API.

    Static cycle detection in lfix-like recurrences is undecidable infull generality. Nevertheless, we might present a restricted interfacewithin which it is decidable. The interaction between laziness anddecidable cyclicity complicates this approach: in general, whether

    a reference is evaluated depends on arbitrary computation. Otherapproaches to this problem might include the use of full dependenttypes or a refinement type system like LiquidHaskell [21]. Inparticular, it seems that many of the constraints we were unableto encode in the Haskell type system might be adequately expressedin a dependent type system augmented with linear or affine types.

    Acknowledgments

    Thank you firstly to Harry Mairson, my advisor at Brandeis, forhis advice and camaraderie. A huge thanks to Dominic Orchard forhis help with proof-reading, equational reasoning techniques, andnumerous pieces of clarifying insight. Without Dan Piponi and hisoutstanding blog, I wouldnt even have set out on this journey.

    Many others have given me ideas, questions, and pointers to great

    papers; among them are Edward Kmett, Jeremy Gibbons, TarmoUustalu, Getty Ritter, Kyle Carter, and Gershom Bazerman. Thanksalso to an anonymous reviewer for noticing an important technicalpoint which Id missed.

    Finally: thank you to Caleb Marcus for putting up with enoughcomonadic lunch-table conversation to fulfill a lifetime quota, toEden Zik for standing outside in the cold to talk about spreadsheets,and to Isabel Ballan for continually reminding me that in the end,its usually to do with Wittgenstein.

  • 7/23/2019 Kenneth Foner - Getting a Quick Fix on Comonads

    12/12

    References

    [1] D. Ahmen, J. Chapman, and T. Uustalu. When is a Containera Comonad? Logical Methods in Computer Science, 10(3),2014.

    [2] N. Alechina, M. Mendler, V. D. Paiva, and E. Ritter.Categorical and Kripke Semantics for Constructive S4 Modal

    Logic. Computer Science Logic, pages 292307, 2001.[3] G. M. Beirman and V. C. V. D. Paiva. Intuitionistic Necessity

    Revisited. Technical report, University of Birmingham,School of Computer Science, 1996.

    [4] S. Capobianco and T. Uustalu. A Categorical Outlook onCellular Automata. arXiv preprint arXiv:1012.1220, 2010.

    [5] C. Done. Twitter Waterflow Problem and Loeb, 2014. URLchrisdone.com/posts/twitter-problem-loeb.

    [6] J. Epstein, A. Black, and S. Peyton-Jones. Towards Haskell inthe Cloud. In ACM SIGPLAN Notices, volume 46, pages118129. ACM, 2011.

    [7] M. Gardner. The Fantastic Combinations of John ConwaysNew Solitaire Game Life. Scientific American, 223(4):120123, October 1970.

    [8] J. Garson. Modal Logic. Stanford Encyclopedia of

    Philosophy, 2014. URLplato.stanford.edu/entries/logic-modal.

    [9] G. Huet. Functional Pearl: The Zipper. Journal of FunctionalProgramming, 7(5):549554, September 1997.

    [10] E. Kmett. The Comonad Package, July 2014. URLhackage.haskell.org/package/comonad.

    [11] E. Kmett. The Distributive Package, May 2014. URLhackage.haskell.org/package/distributive.

    [12] A. Kock. Strong Functors and Monoidal Monads.Archiv derMathematik, 23(1):113120, 1972.

    [13] S. Lindley and C. McBride. Hasochism: the Pleasure and Painof Dependently Typed Haskell Programming. InProceedingsof the 2013 ACM SIGPLAN Symposium on Haskell, pages8192, 2013.

    [14] P. Lindstrom. Notes on Some Fixed Point Constructions inProvability Logic. Journal of Philosophical Logic, 35(3):225230, 2006.

    [15] E. Mendelson. Introduction to Mathematical Logic. CRCPress, 4 edition, 1997.

    [16] D. Menendez. Paper: The Essence of Dataflow Programming.Haskell mailing list archive. URLmail.haskell.org/pipermail/haskell/2005-September/016502.html.

    [17] D. Orchard. Programming Contextual Computations.Technical Report UCAM-CL-TR-854, University ofCambridge, Computer Laboratory, May 2014.

    [18] D. Piponi. From Lobs Theorem to Spreadsheet Evaluation,November 2006. URLblog.sigfpe.com/2006/11/from-l-theorem-to-spreadsheet.html.

    [19] T. Uustalu and V. Vene. Comonadic Notions of Computation.

    Electronic Notes in Theoretical Computer Science, 203(5):263284, 2008.

    [20] T. Uustalu and V. Vene. The Essence of DataflowProgramming. Lecture Notes in Computer Science, 4164:135167, November 2006.

    [21] N. Vazou, E. L. Seidel, R. Jhala, D. Vytiniotis, andS. Peyton-Jones. Refinement Types for Haskell. Proceedingsof the 19th ACM SIGPLAN International Conference on

    Functional Programming, pages 269282, 2014.

    [22] R. Verbrugge. Provability Logic. Stanford Encyclopedia ofPhilosophy, 2010. URLplato.stanford.edu/entries/logic-provability.

    A. Proof:loeb() r fix flip

    loeb() r:: (r(ra) a) raloeb() r =f fix (xfmap($ x)f)

    {-fmap() r = ()-}f fix (x() ($x)f)

    {- inline();-reduce; definition offlip -}f fix (x yflip f x y)){--reduce inner-expression -}

    f fix (flipf){--reduce outer-expression -}

    fix flip

    B. Proof:loebUses Functorial Strength

    loeb::Functorf f (f aa) f aloebf =fix (x fmap($ x)f)

    {- meaning of section notation -}fix (x fmap(flip($) x)f){-curry/uncurryinverses -}

    fix (x fmap(uncurry(flip($)) (, )x)f){-uncurry(flipx) (, )yuncurry x flip(, )y-}fix (x fmap(uncurry($) flip(, )x)f){-fmap(f g) fmapf fmapg(functor law) -}

    fix (x(fmap(uncurry ($)) fmap(flip(, )x))f){- inlineflip;-reduce; use tuple section notation -}

    fix (x(fmap(uncurry ($)) fmap(, x))f){- definition offlex;-reduce -}

    fix (fmap(uncurry ($)) flexf)

    C. Full Listing of the FunctionasNestedAs

    type familyAddNestx where

    AddNest(Nestedfs(f x)) = Nested(Nest fs f)x

    type familyAsNestedAs x ywhereAsNestedAs(f x) (Nested(Flatg)b ) = Nested(Flatf)xAsNestedAsx y=AddNest(AsNestedAsx (UnNesty))

    classNestedAs x ywhere

    asNestedAs::x yAsNestedAs x y

    instance(AsNestedAs(f a) (Nested(Flatg)b )Nested(Flatf)a)

    NestedAs(f a) (Nested(Flatg)b )where

    asNestedAsx =Flatx

    instance(AsNestedAs(f a)(UnNest(Nested(Nestg h)b ))

    Nestedfs(f a),AddNest(Nestedfs(f a))

    Nested(Nestfs f)a,NestedAs(f a) (UnNest(Nested(Nestg h)b )))

    NestedAs(f a) (Nested(Nestg h)b )where

    asNestedAsx y=Nest(asNestedAsx (unNesty))

    D. Haskell Dialect

    The code in this paper relies on a number of language extensionsavailable in GHC 7.8 and later:DataKinds,GADTs,MultiParam-TypeClasses, FlexibleContexts, FlexibleInstances,TypeFamilies,UndecidableInstances,DeriveFunctor, andTupleSections.