{"id":20425305,"url":"https://github.com/catseye/lanthorn","last_synced_at":"2025-07-23T16:08:25.320Z","repository":{"id":65829543,"uuid":"339452936","full_name":"catseye/Lanthorn","owner":"catseye","description":"MIRROR of https://codeberg.org/catseye/Lanthorn : Demonstration of how letrec can be syntactic sugar in a purely functional language","archived":false,"fork":false,"pushed_at":"2023-10-25T12:55:52.000Z","size":99,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-05T04:44:56.377Z","etag":null,"topics":["demonstration","didactic","letrec","pure-functional","source-transformation","syntactic-sugar","syntax-sugar"],"latest_commit_sha":null,"homepage":"https://catseye.tc/node/Lanthorn","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/catseye.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2021-02-16T16:04:35.000Z","updated_at":"2023-10-25T07:47:59.000Z","dependencies_parsed_at":"2023-03-02T01:30:37.090Z","dependency_job_id":"76e426b0-b4ee-4ee4-a0ab-184be9647802","html_url":"https://github.com/catseye/Lanthorn","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/catseye/Lanthorn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLanthorn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLanthorn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLanthorn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLanthorn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/catseye","download_url":"https://codeload.github.com/catseye/Lanthorn/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLanthorn/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266708514,"owners_count":23971946,"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","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["demonstration","didactic","letrec","pure-functional","source-transformation","syntactic-sugar","syntax-sugar"],"created_at":"2024-11-15T07:12:49.839Z","updated_at":"2025-07-23T16:08:25.274Z","avatar_url":"https://github.com/catseye.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"Lanthorn\n========\n\nVersion 1.0 | _Entry_ @ [catseye.tc](https://catseye.tc/node/Lanthorn)\n| _See also:_ [Lariat](https://catseye.tc/node/Lariat)\n* [Iphigeneia](https://catseye.tc/node/Iphigeneia)\n\nWhen I first came across a explanation of how `letrec` works, it was\nin terms of updating references: each of the names is bound to a cell,\nand when the thing that name refers to is eventually defined, that cell\nis updated with that thing.\n\nMy reaction to this was _ugh_.  I mean, sure, it works, but in the\ncontext of functional programming, such an imperative description is\nreally unsatisfying.\n\nSo, I present here a tiny, eager, purely functional language, christened\n**Lanthorn**, whose sole purpose is to host a demonstration of how `letrec`\ncan be written as syntactic sugar over `let` in a purely functional way.\n\nThe transformation is unobtrusive in that it doesn't make any changes in\nthe body of the `letrec`.  The resulting code is not, however, intended\nto be efficient.\n\nSince the language is simple enough and conventional enough that you\ncan probably guess what the programs mean, let's leave the description\nof the language until [Appendix A](#appendix-a), and go straight into\ndescribing the transformation.\n\nDesugaring\n----------\n\n    -\u003e Tests for functionality \"Desugar Lanthorn Program\"\n\nBasically, what we want to do, is take this...\n\n    letrec\n        odd  = fun(x) -\u003e if eq(x, 0) then false else even(sub(x, 1))\n        even = fun(x) -\u003e if eq(x, 0) then true else odd(sub(x, 1))\n    in\n        even(6)\n\n...and turn it into this.\n\n    let\n        odd0  = fun(x, odd1, even1) -\u003e\n                     let\n                         odd = fun(x) -\u003e odd1(x, odd1, even1)\n                         even = fun(x) -\u003e even1(x, odd1, even1)\n                     in\n                         if eq(x, 0) then false else even(sub(x, 1)))\n        even0 = fun(x, odd1, even1) -\u003e\n                     let\n                         odd = fun(x) -\u003e odd1(x, odd1, even1)\n                         even = fun(x) -\u003e even1(x, odd1, even1)\n                     in\n                         if eq(x, 0) then true else odd(sub(x, 1)))\n        odd   = fun(x) -\u003e odd0(x, odd0, even0)\n        even  = fun(x) -\u003e even0(x, odd0, even0)\n    in\n        even(6)\n\nOur evaluator implements this transformation in the\n[Language.Lanthorn.LetRec](src/Language/Lanthorn/LetRec.hs) module.\nHere is what it produces.  Note there is a bit of name-mangling\nadded, compared to the hand-written expansion above.\n\n    letrec\n        odd  = fun(x) -\u003e if eq(x, 0) then false else even(sub(x, 1))\n        even = fun(x) -\u003e if eq(x, 0) then true else odd(sub(x, 1))\n    in\n        even(6)\n    =\u003e let\n    =\u003e   odd$0 = fun(x, odd$1, even$1) -\u003e let\n    =\u003e       odd = fun(x$1) -\u003e odd$1(x$1, odd$1, even$1)\n    =\u003e       even = fun(x$1) -\u003e even$1(x$1, odd$1, even$1)\n    =\u003e     in\n    =\u003e       if eq(x, 0) then false else even(sub(x, 1))\n    =\u003e   even$0 = fun(x, odd$1, even$1) -\u003e let\n    =\u003e       odd = fun(x$1) -\u003e odd$1(x$1, odd$1, even$1)\n    =\u003e       even = fun(x$1) -\u003e even$1(x$1, odd$1, even$1)\n    =\u003e     in\n    =\u003e       if eq(x, 0) then true else odd(sub(x, 1))\n    =\u003e   odd = fun(x) -\u003e odd$0(x, odd$0, even$0)\n    =\u003e   even = fun(x) -\u003e even$0(x, odd$0, even$0)\n    =\u003e in\n    =\u003e   even(6)\n\nIn English, it adds a number of extra parameters to each function in\nthe set of bindings.  Specifically, it adds one parameter for each\nof the bindings.  It then sets up some bindings _inside_ each function\nso that the function uses these parameters for the recursive calls\nit makes.  It also sets up some bindings outside of these functions\nto that the body of the `letrec` sees functions with the original\nparameters they had, hiding all these extra parameters.\n\nRelated Work\n------------\n\nXavier Pinho has written up an alternative way of transforming `letrec`\ninto `let`, using surjective pairing and the Y combinator, in\n[an issue on the Lanthorn project on GitHub](https://github.com/catseye/Lanthorn/issues/1).\n\nAppendix A\n----------\n\n### Basic Syntax of Lanthorn\n\n    -\u003e Tests for functionality \"Pretty-print Lanthorn Program\"\n\nFunction application, numeric literals, string literals.\n\n    add(1, 2)\n    =\u003e add(1, 2)\n\nName binding (`let`) and name reference.\n\n    let a = 1\n        b = 1\n        in zed(a, b)\n    =\u003e let\n    =\u003e   a = 1\n    =\u003e   b = 1\n    =\u003e in\n    =\u003e   zed(a, b)\n\nThe character `$` may not appear in user-supplied names.\n\n    let\n      a$b = 1\n    in\n      zed(a$b)\n    ?\u003e unexpected \"$\"\n\nConditional by boolean (`if`).\n\n    if gt(a, b) then a else b\n    =\u003e if gt(a, b) then a else b\n\nFunction values.\n\n    let up = fun(x) -\u003e add(x, 1) in up(5)\n    =\u003e let\n    =\u003e   up = fun(x) -\u003e add(x, 1)\n    =\u003e in\n    =\u003e   up(5)\n\n### Basic Semantics of Lanthorn\n\n    -\u003e Tests for functionality \"Evaluate Lanthorn Program\"\n\n    1\n    ===\u003e 1\n\n    if true then 5 else 6\n    ===\u003e 5\n\n    let a = 2 in a\n    ===\u003e 2\n\nBasic functions.\n\n    let r = fun(x) -\u003e 77 in r(1)\n    ===\u003e 77\n\n    let r = fun(x) -\u003e x in r(66)\n    ===\u003e 66\n\n`let` is like Scheme's `let*` or Standard ML's `let`:\nlater bindings can see earlier bindings.\n\n    let\n      p = 99\n      r = fun(x) -\u003e p\n    in\n      r(66)\n    ===\u003e 99\n\nNote that depicting a function is implementation-dependent.\n\n    fun(x) -\u003e x\n    ===\u003e \u003c\u003cfunction\u003e\u003e\n\nCan't shadow a binding in `let`.\n\n    let a = 1 in let a = 2 in a\n    ???\u003e Already defined: a\n\nCan't shadow a binding in the formals of a `fun`.\n\n    let r = fun(x, x) -\u003e x in r(10, 10)\n    ???\u003e Already defined: x\n\n    let r = fun(x) -\u003e let x = 3 in x in r(10)\n    ???\u003e Already defined: x\n\n#### `letrec`\n\nBasic usage of `letrec`.\n\n    letrec\n        oddp  = fun(x) -\u003e if eq(x, 0) then false else evenp(sub(x, 1))\n        evenp = fun(x) -\u003e if eq(x, 0) then true else oddp(sub(x, 1))\n    in\n        evenp(6)\n    ===\u003e true\n\n    letrec\n        oddp  = fun(x) -\u003e if eq(x, 0) then false else evenp(sub(x, 1))\n        evenp = fun(x) -\u003e if eq(x, 0) then true else oddp(sub(x, 1))\n    in\n        evenp(5)\n    ===\u003e false\n\n`letrec` nested inside an `if` inside a function definition in an arm of\nanother `letrec`.\n\n    letrec\n        facto = fun(n) -\u003e if eq(n, 1) then 1 else\n            letrec\n                oddp  = fun(x) -\u003e if eq(x, 0) then false else evenp(sub(x, 1))\n                evenp = fun(x) -\u003e if eq(x, 0) then true else oddp(sub(x, 1))\n            in\n                if oddp(n) then\n                    mul(n, facto(sub(n, 1)))\n                else\n                    facto(sub(n, 1))\n    in\n        facto(8)\n    ===\u003e 105\n\n`letrec` nested in the body of another `letrec`.\n\n    letrec\n        oddp  = fun(x) -\u003e if eq(x, 0) then false else evenp(sub(x, 1))\n        evenp = fun(x) -\u003e if eq(x, 0) then true else oddp(sub(x, 1))\n    in\n        letrec facto = fun(n) -\u003e\n            if eq(n, 1) then\n                1\n            else if oddp(n) then\n                mul(n, facto(sub(n, 1)))\n            else\n                facto(sub(n, 1))\n    in\n        facto(8)\n    ===\u003e 105\n\nNested `letrec`, nested right in the arm of another `letrec`.  Currently,\nthis is an error, because the inner scope cannot \"see\" the outer `letrec`.\nThough I'm not yet convinced of what the most reasonable behaviour is here.\n\n    letrec\n        facto =\n            letrec\n                oddp  = fun(x) -\u003e if eq(x, 0) then false else evenp(sub(x, 1))\n                evenp = fun(x) -\u003e if eq(x, 0) then true else oddp(sub(x, 1))\n            in\n                fun(n) -\u003e if eq(n, 1) then 1 else\n                    if oddp(n) then\n                        mul(n, facto(sub(n, 1)))\n                    else\n                        facto(sub(n, 1))\n    in\n        facto(8)\n    ???\u003e Not in scope: facto\n\n`letrec` nested inside a function definition inside an arm of a plain `let`.\n\n    let\n        factoo = fun(f, n) -\u003e\n            letrec\n                oddp  = fun(x) -\u003e if eq(x, 0) then false else evenp(sub(x, 1))\n                evenp = fun(x) -\u003e if eq(x, 0) then true else oddp(sub(x, 1))\n            in\n                if eq(n, 1) then 1 else\n                    if oddp(n) then\n                        mul(n, f(f, sub(n, 1)))\n                    else\n                        f(f, sub(n, 1))\n    in\n        factoo(factoo, 7)\n    ===\u003e 105\n\n`letrec` nested inside body of a plain `let`.\n\n    let\n        factopen = fun(f, n) -\u003e if eq(n, 1) then 1 else mul(n, f(f, sub(n, 1)))\n        target = 7\n    in\n        letrec\n            oddp  = fun(x) -\u003e if eq(x, 0) then false else evenp(sub(x, 1))\n            evenp = fun(x) -\u003e if eq(x, 0) then true else oddp(sub(x, 1))\n        in\n            if oddp(target) then factopen(factopen, target) else 0\n    ===\u003e 5040\n\n`letrec` works on functions that have more than one argument.\n\n    letrec\n        oddsump  = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then false else evensump(sub(x, 1), y, z)\n        evensump = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then true else oddsump(sub(x, 1), y, z)\n    in\n        evensump(5,3,1)\n    ===\u003e false\n\n    letrec\n        oddsump  = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then false else evensump(sub(x, 1), y, z)\n        evensump = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then true else oddsump(sub(x, 1), y, z)\n    in\n        evensump(6,3,1)\n    ===\u003e true\n\n`letrec` works on functions which use different argument names.\n\n    letrec\n        oddsump  = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then false else evensump(sub(x, 1), y, z)\n        evensump = fun(p,q,r) -\u003e if eq(add(p, add(q, r)), add(q, r)) then true else oddsump(sub(p, 1), q, r)\n    in\n        evensump(5,3,1)\n    ===\u003e false\n\n    letrec\n        oddsump  = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then false else evensump(sub(x, 1), y, z)\n        evensump = fun(p,q,r) -\u003e if eq(add(p, add(q, r)), add(q, r)) then true else oddsump(sub(p, 1), q, r)\n    in\n        evensump(6,3,1)\n    ===\u003e true\n\n`letrec` works on functions that have differing numbers of arguments.\n\n    letrec\n        oddsump  = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then false else evensump(sub(x, 1), add(y, z))\n        evensump = fun(p,q)   -\u003e if eq(add(p, q), q) then true else oddsump(sub(p, 1), 1, sub(q, 1))\n    in\n        oddsump(5,3,1)\n    ===\u003e true\n\n### Properties of the `letrec` transformation\n\n    -\u003e Tests for functionality \"Desugar Lanthorn Program\"\n\nWhen a `letrec` is desugared, the generated functions have argument\nnames that are based on the original argument names.  Also, the\ninnermost `let`s bind the plain names to functions with the same arity\nas the original functions.\n\n    letrec\n        oddsump  = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then false else evensump(sub(x, 1), y, z)\n        evensump = fun(x,y,z) -\u003e if eq(add(x, add(y, z)), add(y, z)) then true else oddsump(sub(x, 1), y, z)\n    in\n        evensump(5,3,1)\n    =\u003e let\n    =\u003e   oddsump$0 = fun(x, y, z, oddsump$1, evensump$1) -\u003e let\n    =\u003e       oddsump = fun(x$1, y$1, z$1) -\u003e oddsump$1(x$1, y$1, z$1, oddsump$1, evensump$1)\n    =\u003e       evensump = fun(x$1, y$1, z$1) -\u003e evensump$1(x$1, y$1, z$1, oddsump$1, evensump$1)\n    =\u003e     in\n    =\u003e       if eq(add(x, add(y, z)), add(y, z)) then false else evensump(sub(x, 1), y, z)\n    =\u003e   evensump$0 = fun(x, y, z, oddsump$1, evensump$1) -\u003e let\n    =\u003e       oddsump = fun(x$1, y$1, z$1) -\u003e oddsump$1(x$1, y$1, z$1, oddsump$1, evensump$1)\n    =\u003e       evensump = fun(x$1, y$1, z$1) -\u003e evensump$1(x$1, y$1, z$1, oddsump$1, evensump$1)\n    =\u003e     in\n    =\u003e       if eq(add(x, add(y, z)), add(y, z)) then true else oddsump(sub(x, 1), y, z)\n    =\u003e   oddsump = fun(x, y, z) -\u003e oddsump$0(x, y, z, oddsump$0, evensump$0)\n    =\u003e   evensump = fun(x, y, z) -\u003e evensump$0(x, y, z, oddsump$0, evensump$0)\n    =\u003e in\n    =\u003e   evensump(5, 3, 1)\n\nThe transformation mangles names that it generates so that they never\nshadow names that appear in the user's program.  (Names containing `$` may\nnot appear in a user-supplied program.)\n\n    let\n        odd0 = fun(a, b, c) -\u003e a\n    in\n        letrec\n            odd  = fun(x) -\u003e if eq(x, 0) then false else even(sub(x, 1))\n            even = fun(x) -\u003e if eq(x, 0) then true else odd(sub(x, 1))\n        in\n            even(6)\n    =\u003e let\n    =\u003e   odd0 = fun(a, b, c) -\u003e a\n    =\u003e in\n    =\u003e   let\n    =\u003e     odd$0 = fun(x, odd$1, even$1) -\u003e let\n    =\u003e         odd = fun(x$1) -\u003e odd$1(x$1, odd$1, even$1)\n    =\u003e         even = fun(x$1) -\u003e even$1(x$1, odd$1, even$1)\n    =\u003e       in\n    =\u003e         if eq(x, 0) then false else even(sub(x, 1))\n    =\u003e     even$0 = fun(x, odd$1, even$1) -\u003e let\n    =\u003e         odd = fun(x$1) -\u003e odd$1(x$1, odd$1, even$1)\n    =\u003e         even = fun(x$1) -\u003e even$1(x$1, odd$1, even$1)\n    =\u003e       in\n    =\u003e         if eq(x, 0) then true else odd(sub(x, 1))\n    =\u003e     odd = fun(x) -\u003e odd$0(x, odd$0, even$0)\n    =\u003e     even = fun(x) -\u003e even$0(x, odd$0, even$0)\n    =\u003e   in\n    =\u003e     even(6)\n\n    -\u003e Tests for functionality \"Evaluate Lanthorn Program\"\n\n    letrec\n        odd  = fun(x) -\u003e if eq(x, 0) then false else even(sub(x, 1))\n        odd0 = fun(a, b, c) -\u003e a\n        even = fun(x) -\u003e if eq(x, 0) then true else odd(sub(x, 1))\n    in\n        even(6)\n    ===\u003e true\n\nYou might think that instead of mangling names, we could just allow shadowing\nin the language.  But that by itself doesn't solve our problem, since you\ncould still say something like the following.  The `letrec` desugaring would\nhave to be more aware of how it constructs names, at any rate, in order to\navoid the conflict here.  And mangling is the simplest way to do that.\n\n    -\u003e Tests for functionality \"Desugar Lanthorn Program\"\n\n    letrec\n        odd  = fun(x) -\u003e if eq(x, 0) then false else even(sub(x, 1))\n        odd0 = fun(a, b, c) -\u003e a\n        even = fun(x) -\u003e if eq(x, 0) then true else odd(sub(x, 1))\n    in\n        even(6)\n    =\u003e let\n    =\u003e   odd$0 = fun(x, odd$1, odd0$1, even$1) -\u003e let\n    =\u003e       odd = fun(x$1) -\u003e odd$1(x$1, odd$1, odd0$1, even$1)\n    =\u003e       odd0 = fun(a$1, b$1, c$1) -\u003e odd0$1(a$1, b$1, c$1, odd$1, odd0$1, even$1)\n    =\u003e       even = fun(x$1) -\u003e even$1(x$1, odd$1, odd0$1, even$1)\n    =\u003e     in\n    =\u003e       if eq(x, 0) then false else even(sub(x, 1))\n    =\u003e   odd0$0 = fun(a, b, c, odd$1, odd0$1, even$1) -\u003e let\n    =\u003e       odd = fun(x$1) -\u003e odd$1(x$1, odd$1, odd0$1, even$1)\n    =\u003e       odd0 = fun(a$1, b$1, c$1) -\u003e odd0$1(a$1, b$1, c$1, odd$1, odd0$1, even$1)\n    =\u003e       even = fun(x$1) -\u003e even$1(x$1, odd$1, odd0$1, even$1)\n    =\u003e     in\n    =\u003e       a\n    =\u003e   even$0 = fun(x, odd$1, odd0$1, even$1) -\u003e let\n    =\u003e       odd = fun(x$1) -\u003e odd$1(x$1, odd$1, odd0$1, even$1)\n    =\u003e       odd0 = fun(a$1, b$1, c$1) -\u003e odd0$1(a$1, b$1, c$1, odd$1, odd0$1, even$1)\n    =\u003e       even = fun(x$1) -\u003e even$1(x$1, odd$1, odd0$1, even$1)\n    =\u003e     in\n    =\u003e       if eq(x, 0) then true else odd(sub(x, 1))\n    =\u003e   odd = fun(x) -\u003e odd$0(x, odd$0, odd0$0, even$0)\n    =\u003e   odd0 = fun(a, b, c) -\u003e odd0$0(a, b, c, odd$0, odd0$0, even$0)\n    =\u003e   even = fun(x) -\u003e even$0(x, odd$0, odd0$0, even$0)\n    =\u003e in\n    =\u003e   even(6)\n\n    -\u003e Tests for functionality \"Evaluate Lanthorn Program\"\n\n    let\n        odd0 = fun(a, b, c) -\u003e a\n    in\n        letrec\n            odd  = fun(x) -\u003e if eq(x, 0) then false else even(sub(x, 1))\n            even = fun(x) -\u003e if eq(x, 0) then true else odd(sub(x, 1))\n        in\n            even(6)\n    ===\u003e true\n\nNote that there is probably a case where a `letrec` nested another `letrec`, and\nwhich shadows variables of the enclosing `letrec`, produces a less readable\nerror message about shadowing, because it mentions the mangled names; but\nI can live with that for now.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatseye%2Flanthorn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcatseye%2Flanthorn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatseye%2Flanthorn/lists"}