{"id":16879445,"url":"https://github.com/purefunctor/purescript-ssrs","last_synced_at":"2026-01-04T16:08:02.445Z","repository":{"id":40489532,"uuid":"412327685","full_name":"purefunctor/purescript-ssrs","owner":"purefunctor","description":"Stack-safe recursion schemes on dissectible data structures.","archived":false,"fork":false,"pushed_at":"2022-05-06T04:16:30.000Z","size":97,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-25T04:44:45.776Z","etag":null,"topics":["purescript","recursion-schemes"],"latest_commit_sha":null,"homepage":"","language":"PureScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/purefunctor.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-01T04:37:30.000Z","updated_at":"2022-06-26T16:14:51.000Z","dependencies_parsed_at":"2022-08-26T12:34:26.018Z","dependency_job_id":null,"html_url":"https://github.com/purefunctor/purescript-ssrs","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purefunctor%2Fpurescript-ssrs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purefunctor%2Fpurescript-ssrs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purefunctor%2Fpurescript-ssrs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purefunctor%2Fpurescript-ssrs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/purefunctor","download_url":"https://codeload.github.com/purefunctor/purescript-ssrs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244513106,"owners_count":20464592,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["purescript","recursion-schemes"],"created_at":"2024-10-13T15:54:23.910Z","updated_at":"2026-01-04T16:08:02.392Z","avatar_url":"https://github.com/purefunctor.png","language":"PureScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# purescript-ssrs\n\nStack-safe recursion schemes through\n[dissectible](https://github.com/PureFunctor/purescript-dissect) data\nstructures. This package provides the same API that\n[matryoshka](https://github.com/purescript-contrib/purescript-matryoshka)\nhas minus the `Recursive` / `Corecursive` type classes, distributive\nlaws, and generalized recursion schemes that make use of said laws.\n\n## Installation\n\nUsing `spago`:\n\n    $ spago install ssrs\n\nor if not present within the current package set, add it to\n`packages.dhall`:\n\n``` dhall\nlet upstream =\n      https://github.com/purescript/package-sets/releases/download/psc-0.14.4-20211005/packages.dhall\n        sha256:2ec351f17be14b3f6421fbba36f4f01d1681e5c7f46e0c981465c4cf222de5be\n\nlet overrides = {=}\n\nlet additions =\n      { dissect =\n        { dependencies = [ ... ]  -- copy dependencies from spago.dhall\n        , repo = \"https://github.com/PureFunctor/purescript-ssrs.git\"\n        , version = \"\u003cinsert-desired-revision-here\u003e\"\n        }\n      }\n\nin  upstream // overrides // additions\n```\n\n## Rationale\n\nI originally encountered the implementation of a tail-recursive\ncatamorphism described in the paper [Clowns to the Left of me, Jokers to\nthe Right (Pearl): Dissecting Data\nStructures](https://dl.acm.org/doi/abs/10.1145/1328438.1328474) by Conor\nMcBride. After a few more days of research, I eventually figured out how\nto transpose said algorithm from a catamorphism into an anamorphism, and\nsubsequently, I've also synthesized a hylomorphism by fusing the two\nalgorithms together.\n\nFor the greater family of recursion schemes however, I had a bit of help\nusing \u003chttps://github.com/willtim/recursion-schemes/\u003e to derive them\nfrom the `cata`, `ana`, and `hylo` schemes. I've decided to compose the\nother recursion schemes against these three instead defining them\nspecifically in order to reduce the overall size of the package.\nLikewise, it's also easier to derive schemes these way as they generally\nfollowed the common pattern of: *turn this GAlgebra into an Algebra,\nfeed it into cata, and unwrap the result afterwards* or *turn this\nGCoalgebra into a Coalgebra, wrap some input, then feed it into ana*.\n\n## An Iterative Catamorphism Machine\n\nIn the same vein as [the quick primer to\ndissect](https://github.com/PureFunctor/purescript-dissect#quick-primer-on-dissect),\nI'll be assuming that the reader has some familiarity working with\nfixed-point functors, recursion schemes, and dissections. Similarly, I\nwon't dwell too much on explaining other concepts in-depth.\n\nThe original paper implements a tail-recursive catamorphism using four\nseparate functions that form a tail-recursive loop. An optimizing\ncompiler can take these four functions and identify the tail-recursion\nthat forms within them.\n\n``` purescript\ncata ∷ ∀ p q v. Dissect p q ⇒ (p v → v) → Mu p → v\ncata algebra t = load algebra t Nil\n\nload ∷ ∀ p q v. Dissect p q ⇒ (p v → v) → Mu p → List (q v (Mu p)) → v\nload algebra (In pt) stk = next algebra (right (Left pt)) stk\n\nnext ∷ ∀ p q v. Dissect p q ⇒ (p v → v) → Either (Tuple (Mu p) (q v (Mu p))) (p v) → List (q v (Mu p)) → v\nnext algebra (Left (t, pd)) stk = load algebra t (pd : stk)\nnext algebra (Right pv) stk = unload algebra (algebra pv) stk\n\nunload ∷ ∀ p q v. Dissect p q ⇒ (p v → v) → v → List (q v (Mu p)) → v\nunload algebra v (pd : stk) = next algebra (right (Right (pd, v))) stk\nunload algebra v Nil = v\n```\n\nUnfortunately, PureScript is not one of those compilers (yet, but\nhopefully), and so I had to fuse them together to help the compiler with\ntail-call optimization. This definition is a bit terse, but I'll try to\nexplain the intuition behind it in the next few paragraphs.\n\n``` purescript\ncata ∷ ∀ p q v. Dissect p q ⇒ Algebra p v → Mu p → v\ncata algebra (In pt) = go (pluck pt) Nil\n  where\n  go :: Either (Tuple (Mu p) (q v (Mu p))) (p v) → List (q v (Mu p)) → v\n  go index stack =\n    case index of\n      Left (Tuple (In pt') pd) →\n        go (pluck pt') (pd : stack)\n      Right pv →\n        case stack of\n          (pd : stk) →\n            go (plant pd (algebra pv)) stk\n          Nil →\n            algebra pv\n```\n\n`Dissect` allows us to build iterative machines that model the traversal\nof a recursive structure. What we've essentially done at this point is\nmodel a catamorphism as an iterative machine that uses a stack for its\nstate. Before we proceed further, let's take a look at our toolkit and\nhow we can use it to build an iterative catamorphism machine from\nscratch.\n\n1.  We have some recursive data type `Mu p ~ p (Mu p)`, and an algebra\n    `p v → v`.\n\n2.  We want to take `Mu p ~ p (Mu p)` and pluck all seats of `Mu p` and\n    plant the resulting holes with `v`. This allows us to then call our\n    algebra on the resulting `p v`.\n\n3.  When plucking a `Mu p ~ p (Mu p)` we end up with two choices:\n\n    1.  We receive a fruit `Mu p ~ p (Mu p)` and a dissection\n        `q v (Mu p)`. We have to process this fruit as we did in in step\n        2, and store the dissection somewhere until we can fill it with\n        the processed fruit.\n\n    2.  We receive a flower `p v` that we can call our algebra on,\n        giving us a `v`. We can then use this value to fill in the\n        dissections that we've stored in step 3.1 or if we have none, we\n        can simply return the value.\n\nIf this sounds like an iterative traversal of a tree to you, then you're\ngaining the right intuition, otherwise, it's good to know at this point\nin time. Iterative traversals make use of a stack in order to keep track\nof items being visited at each level. What we're interested in however\nis storing dissections in the stack and popping them out once we have\nthe appropriate values to fill them in with later.\n\nLet's contextualize this into a specific type:\n\n``` purescript\ndata TreeF n = Leaf | Fork n n\n\ntype Tree = Mu TreeF\n```\n\nSuppose that we have the following structure and an empty stack:\n\n``` purescript\nFork [ Fork [ Leaf - Leaf ] - Leaf ]\n\nStack []\n```\n\nBy calling `pluck` on this structure, we dissect it into two parts. For\nnow, we push the dissection onto the stack.\n\n``` purescript\n\u003e pluck $ Fork [ Fork [ Leaf - Leaf ] - Leaf ]\nFork [ Leaf - Leaf ], Fork [ () - Leaf ]\n\n\u003e push $ Fork [ () - Leaf ]\nStack [ Fork [ () - Leaf ] ]\n```\n\nWe then call `pluck` on the result, and we end up with another\ndissection that we have to push.\n\n``` purescript\n\u003e pluck $ Fork [ Leaf - Leaf ]\nLeaf, Fork [ () - Leaf ]\n\n\u003e push $ Fork [ () - Leaf ]\nStack [ Fork [ () - Leaf ]\n      , Fork [ () - Leaf ]\n      ]\n```\n\nWe call `pluck` again on the result, but this time, we reach a base case\nthat our `algebra` gladly accepts. Furthermore, we can `plant` this\nvalue in the top-most dissection in our stack.\n\n``` purescript\n\u003e pluck Leaf\nPv\n\n\u003e algebra Pv\nV\n\n\u003e pop Stack\nFork [ () - Leaf ]\n\n\u003e plant $ Fork [ () - Leaf ] $ V\nLeaf, Fork [ V - () ]\n```\n\nBy planting a value, we get the next element to pluck and the next\ndissection to push. Since we receive yet another base case, we're able\nto plant it immediately to the top-most dissection. Finally, we've\nmanaged to replace all recursive seats and turn them into collapsed\nvalues. Likewise, we can call our algebra on this structure to collapse\nit further.\n\n``` purescript\n\u003e push $ Fork [ V - () ]\nStack [ Fork [ () - Leaf ]\n      , Fork [ V - () ]\n      ]\n\n\u003e pluck Leaf\nPv\n\n\u003e algebra Pv\nV\n\n\u003e pop $ Stack\nFork [ V - () ]\n\n\u003e plant $ Fork [ V - () ] $ V\nFork [ V - V ]\n\n\u003e algebra $ Fork [ V - V ]\nV\n```\n\nWe're not quite done yet however, as we still have items in the stack.\nI'll let the pseudo-REPL do the talking from here on, but in the end of\nthis session, we should have our final result.\n\n``` purescript\n\u003e pop $ Stack\nFork [ () - Leaf ]\n\n\u003e plant $ Fork [ () - Leaf ] $ V\nLeaf, Fork [ V - () ]\n\n\u003e push $ Fork [ V - () ]\nStack [ Fork [ V - () ]\n      ]\n\n\u003e pluck Leaf\nPv\n\n\u003e algebra Pv\nV\n\n\u003e pop $ Stack\nFork [ V - () ]\n\n\u003e plant $ Fork [ V - () ] $ V\nFork [ V - V ]\n\n\u003e algebra $ Fork [ V - V ]\nV\n```\n\nWe can express this imperative algorithm in pseudocode like so.\n\n``` purescript\nLET Index = Pluck(Start)\nLET Stack = []\n\nLOOP\n  IF Index IS [Next, Hole]\n    Push(Hole, Stack)\n    Index = Pluck(Next)\n  ELSE IF Index IS Base\n    IF Pop(Stack) IS Hole\n      Index = Plant(Hole, Algebra(Base))\n    ELSE\n      DONE Algebra(Base)\n    END\n  END\nEND\n```\n\n## From Catamorphisms to Anamorphisms and Hylomorphisms\n\nTraditionally, catamorphisms can be implemented as a series of function\ncompositions that go from `Mu\np` into a `v`. The code block below adopts the Haskell definition listed\nin [Recursion Schemes, Part II: A Mob of\nMorphisms](https://blog.sumtypeofway.com/posts/recursion-schemes-part-2.html).\nNote that in order to actually work with this definition, we'd have to\nperform some indirection as to not implicit perform left-recursion in\n`cata`; see the implementation in\n[matryoshka](https://github.com/purescript-contrib/purescript-matryoshka)\nfor more details.\n\n``` purescript\ncata :: forall f a. Functor f =\u003e (f a -\u003e a) -\u003e (Mu f -\u003e a)\ncata f = unwrap \u003e\u003e\u003e fmap (cata f) \u003e\u003e\u003e f\n```\n\nAnamorphisms are the dual of catamorphisms; likewise, their coalgebras\nand algebras are also duals. If we flip all relevant arrows in this\ndefinition, we end up with:\n\n``` purescript\nana :: forall f a. Functor a =\u003e (a -\u003e f a) -\u003e (a -\u003e Mu f)\nana f = wrap \u003c\u003c\u003c fmap (ana f) \u003c\u003c\u003c f\n```\n\nWe can apply the same principle with our iterative catamorphic machine.\nIf we contextualize *flipping the arrows* in our implementation, we find\nout that replacing all instances of `Mu p` unwrapping is replaced with a\ncall to `coalgebra`, while all calls to `algebra` are replaced with\n`Mu p` wrapping.\n\n``` purescript\nana ∷ ∀ p q v. Dissect p q ⇒ Coalgebra p v → v → Mu p\nana coalgebra seed = go (pluck (coalgebra seed)) Nil\n  where\n  go :: Either (Tuple v (q (Mu p) v)) (p (Mu p)) → List (q (Mu p) v) → Mu p\n  go index stack =\n    case index of\n      Left (Tuple pt pd) →\n        go (pluck (coalgebra pt)) (pd : stack)\n      Right pv →\n        case stack of\n          (pd : stk) →\n            go (plant pd (In pv)) stk\n          Nil →\n            In pv\n```\n\nFor the pseudocode:\n\n``` purescript\nLET Index = Pluck(Coalgebra(Seed))\nLET Stack = []\n\nLOOP\n  IF Index IS [Next, Hole]\n    Push(Hole, Stack)\n    Index = Pluck(Coalgebra(Next))\n  ELSE IF Index IS Recr\n    IF Pop(Stack) IS Hole\n      Index = Plant(Hole, Mu(Recr))\n    ELSE\n      DONE Mu(Recr)\n    END\n  END\nEND\n```\n\nHylomorphisms can be defined as the composition of a catamorphism and an\nanamorphism. While convenient to define, we unfortunately have to pay\nthe cost of keeping the entire intermediate structure built by the\nanamorphism before it can be folded by the catamorphism. We can\nalleviate this by \"fusing\" these two loops together to form a single\ntight loop. Our definition for an iterative hylomorphism machine looks\nlike:\n\n``` purescript\nhylo ∷ ∀ p q v w. Dissect p q ⇒ Algebra p v → Coalgebra p w → w → v\nhylo algebra coalgebra seed = go (pluck (coalgebra seed)) Nil\n  where\n  go :: Either (Tuple w (q v w)) (p v) → List (q v w) → v\n  go index stack =\n    case index of\n      Left (Tuple pt pd) →\n        go (pluck (coalgebra pt)) (pd : stack)\n      Right pv →\n        case stack of\n          (pd : stk) →\n            go (plant pd (algebra pv)) stk\n          Nil →\n            algebra pv\n```\n\nIf we analyze the implementation, what we've done is replace the\n\"planting\" branch in our anamorphism with the branch that the\ncatamorphism machine uses. In turn, we're able to unfold structures and\nfold them at each recursive level, instead of waiting for the entire\nrecursive structure to unfold.\n\nAs for the pseudocode:\n\n``` purescript\nLET Index = Pluck(Coalgebra(Seed))\nLET Stack = []\n\nLOOP\n  IF Index IS [Next, Hole]\n    Push(Hole, Stack)\n    Index = Pluck(Coalgebra(Next))\n  ELSE IF Index IS Base\n    IF Pop(Stack) IS Hole\n      Index = Plant(Hole, Algebra(Base))\n    ELSE\n      DONE Algebra(Base)\n    END\n  END\nEND\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurefunctor%2Fpurescript-ssrs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpurefunctor%2Fpurescript-ssrs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurefunctor%2Fpurescript-ssrs/lists"}