{"id":20425289,"url":"https://github.com/catseye/lariat","last_synced_at":"2025-06-23T07:02:24.079Z","repository":{"id":40342851,"uuid":"438793924","full_name":"catseye/Lariat","owner":"catseye","description":"MIRROR of https://codeberg.org/catseye/Lariat : An abstract data type for lambda terms","archived":false,"fork":false,"pushed_at":"2023-11-09T18:09:37.000Z","size":56,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-05T04:45:04.176Z","etag":null,"topics":["abstract-data-type","abstract-name-binding","lambda-terms","name-binding"],"latest_commit_sha":null,"homepage":"https://catseye.tc/node/Lariat","language":"Haskell","has_issues":false,"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/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,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-15T22:44:07.000Z","updated_at":"2023-10-24T18:46:59.000Z","dependencies_parsed_at":"2025-01-15T15:19:34.247Z","dependency_job_id":null,"html_url":"https://github.com/catseye/Lariat","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/catseye/Lariat","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLariat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLariat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLariat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLariat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/catseye","download_url":"https://codeload.github.com/catseye/Lariat/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catseye%2FLariat/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261433935,"owners_count":23157197,"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":["abstract-data-type","abstract-name-binding","lambda-terms","name-binding"],"created_at":"2024-11-15T07:12:47.275Z","updated_at":"2025-06-23T07:02:19.062Z","avatar_url":"https://github.com/catseye.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"Lariat\n======\n\n_Version 0.3_\n\n**Lariat** is a project to define a total abstract data type for\nproper lambda terms, consisting of four basic operations:\n`app`, `abs`, `var`, and `destruct`.\n\nThis repository presents the definition of these operations.  It also\ncontains implementations of this abstract data type in various\nprogramming languages, currently including:\n\n*   [Haskell](impl/Haskell/)\n\nThe version of the Lariat defined by this document is 0.3.  This\nversion number will be promoted to 1.0 once vetted sufficiently.\n\n#### Table of Contents\n\n*   [Background](#background)\n*   [The Operations](#the-operations)\n*   [Some Examples](#some-examples)\n*   [Discussion](#discussion)\n\nBackground\n----------\n\nThere are several approaches to representing lambda terms in software.\n\nThe naive approach is to represent them just as they are written on paper.  In this approach, whether\na variable, such as _x_, is free or bound depends on whether it is inside a lambda abstraction\nλ _x_ or not.  If you need to manipulate it (or the abstraction it's bound to), you might need\nto rename it so that it doesn't conflict with another variable also called _x_ that is perhaps\nfree or perhaps bound to a different lambda abstraction.\n\nThis is tiresome and error-prone.  So other approaches were developed.\n\nOne such alternate approach is [De Bruijn indexes](https://en.wikipedia.org/wiki/De_Bruijn_index) (Wikipedia),\nwhere variables are represented not by names, but by numbers.\nThe number indicates which lambda abstraction the variable is bound to, if any;\na 1 indicates the immediately enclosing lambda abstraction, a 2 indicates the lambda abstraction\njust above that, and so on.  If the number exceeds the number of enclosing lambda abstractions,\nthen it is a free variable.\n\nBut this, too, has some drawbacks, so people have devised a number of other approaches:\n\n*   \"maps\" ([Viewing Terms through Maps](https://www.mathematik.uni-muenchen.de/~schwicht/papers/lambda13/lamtheory8.pdf) (PDF), Sato et al., 1980)\n    (see also [these slides](https://www.fos.kuis.kyoto-u.ac.jp/~masahiko/papers/mask.pdf) (PDF) from 2012)\n*   \"nominal techniques\" ([A New Approach to Syntax](http://www.gabbay.org.uk/papers/newaas.pdf) (PDF), Gabbay and Pitts, 1999)\n*   \"locally nameless\" ([I am not a number](http://www.e-pig.org/downloads/notanum.pdf) (PDF), McBride and McKinna, 2004)\n*   \"bound\" ([bound: Making de Bruijn Succ Less](https://www.schoolofhaskell.com/user/edwardk/bound), Kmett, 2013)\n\namong others.\n\nBut the point I would like to make in this article is this:  At some level of abstraction\n_it does not matter_ which approach is chosen _as long as_ the approach satisfies the\nessential properties that we require of lambda terms.\n\nTo this end, this article presents an abstract data type (ADT) for lambda terms, which we\ncall **Lariat**, consisting of four operations.  The actual, concrete data structure\nin which they are stored, and the actual, concrete mechanism by which names\nbecome bound to terms, are of no consequence (and may well be hidden\nfrom the programmer) so long as the implementation of the operations conforms\nto the stated specification.\n\nThis ADT is designed for simplicity and elegance rather than performance.  It is a minimal\nformulation that does not necessarily make any of commonly-used manipulations\nof lambda terms efficient.\n\nThis ADT has two properties, intended to contribute to its elegance.\nFirstly, it can represent only _proper_ lambda terms; that is, it is not possible\nfor a lambda term constructed by the Lariat operations to contain an invalid\nbound variable.\n\nSecondly, it is _total_ in the sense that all operations are defined\nfor all inputs that conform to their type signatures.  There are no conditions\n(such as trying to pop from an empty stack in a stack ADT) where the result is\nundefined, nor any defined to return an error condition.  This totality does, however,\ncome at the cost of the operations being higher-order and with polymorphic types.\n\nFor more background information, see the [Discussion](#discussion) section below.\n\nNames\n-----\n\nLambda terms are essentially about name binding, and in any explication of name binding,\nwe must deal with names.  As of 0.3, Lariat requires only two properties of names.\n\nFirstly, it must be possible to compare two names for equality.  This is\nrequired for operations that replace free variables that have a given name\nwith a value -- there must be some way for them to check that the free\nvariable has the name that they are seeking.\n\nSecondly, given a set of names, it must be possible to generate a new name that\nis not equal to any of the names in the set (a so-called \"fresh\" name).  This is\nrequired to properly implement the `destruct` operation.  If names are modelled\nas character strings, obtaining a fresh name could be as simple as finding the\nlongest string of a set of strings, and prepending `\"a\"` to it.\n\nNote that, although neither of these properties is exposed as an operation,\nit would be reasonable for a practical implementation of Lariat to expose\nthem so.  It would also be reasonable to provide\nother operations on names, such as constructing a new name from a textual\nrepresentation, rendering a given name to a canonical textual representation,\nand so forth.  From the perspective of Lariat itself these are ancillary\noperations, and as such will not be defined in this document.\n\nTerms\n-----\n\nWe now list the four operations available for manipulating terms.\n\n### `var(n: name): term`\n\nGiven a name _n_, return a _free variable_ with the name _n_.\n\n\u003e **Note**: A free variable is a term; it can be passed to any operation\n\u003e that expects a term.\n\n### `app(t: term, u: term): term`\n\nGiven a term _t_ and a term _u_, return an _application term_\nwhich contains _t_ as its first subterm and _u_ as its second\nsubterm.\n\n\u003e **Note**: An application term is a term that behaves just like an\n\u003e ordered pair of terms.\n\n### `abs(n: name, t: term): term`\n\nGiven a name _n_ and a term _t_, return an _abstraction term_\ncontaining _t_', where _t_' is a version of _t_ where all free\nvariables named _n_ inside _t_' have been replaced with\nbound variables.  These bound variables are bound to\nthe returned abstraction term.\n\n\u003e **Note**: we may consider a bound variable to be a term, but\n\u003e the user of the abstract data type cannot work with bound variables\n\u003e directly, so it is unlike all other kinds of terms in that respect.\n\u003e A bound variable is always bound to a particular abstraction term.\n\u003e In the case of `abs`, the abstraction term to which variables are\n\u003e bound is always the term returned by the `abs` operation.\n\n\u003e **Note**: an abstraction term contains one subterm.  This\n\u003e subterm cannot be extracted directly, as it may contain bound\n\u003e variables, which the user cannot work with directly.\n\n### `destruct(t: term, f1: fun, f2: fun, f3: fun): X`\n\nGiven a term _t_ and three functions _f1_, _f2_, and _f3_\n(each with a different signature, described below),\nchoose one of the three functions based on the structure of\n_t_, and evaluate it, returning what it returns.\n\nIf _t_ is a free variable, evaluate _f1_(_n_) where _n_ is the name\nof the free variable _t_.\n\nIf _t_ is an application term, evaluate _f2_(_u_, _v_) where _u_ is\nthe first subterm of _t_ and _v_ is the second subterm of _t_.\n\nIf _t_ is an abstraction term, evaluate _f3_(_u_, _n_) where _u_ is\na version of _t_ where all bound variables in _u_ that were bound to\n_u_ itself have been replaced by _n_, where _n_ is a fresh name\n(i.e. a name that does not occur in any free variable in any subterm\nof _u_).\n\n\u003e **Note**: as stated above, a bound variable is always bound\n\u003e to an abstraction term.  The bound variables that are replaced by the\n\u003e `destruct` of an abstraction term are always and only those that are\n\u003e bound to the abstraction term being `destruct`ed.\n\n\u003e **Note**: the `destruct` operation's signature shown above was abbreviated to make\n\u003e it less intimidating.  The full signature would be\n\u003e \n\u003e     destruct(t: term, f1: fun(n: name): X, f2: fun(u: term, v: term): X, f3: fun(u: term, n: name): X): X\n\u003e \n\n\u003e **Note**: see the section on \"Names\" above for the basic\n\u003e requirements for obtaining a fresh name.\n\nSome Examples\n-------------\n\nWe will now give some concrete examples of how these operations\ncan be used.  But first, we would like to emphasize that\nLariat is an ADT for lambda _terms_, not the lambda\n_calculus_.  Naturally, one ought to be able to write a lambda calculus\nnormalizer using these operations (and this will be one of our goals in\nthe next section), but one is not restricted to that activity.  The terms\nconstructed using the Lariat operations may be used for any purpose\nfor which terms-with-name-binding might be useful.\n\n### Example 1\n\nA common task is to obtain the set of free variables present\nin a lambda term.  This is not difficult; we only need to\nremember that every time we `destruct` an abstraction term, we\nintroduce a fresh free variable of our own, to keep track of\nthese, and make sure not to include any of them when we\nreport the free variables we found.\n\n\u003e **Note**: In the following pseudocode, `+` is the set union operator.\n\n    let freevars = fun(t, ours) -\u003e\n        destruct(t,\n            fun(n) -\u003e if n in ours then {} else {n},\n            fun(u, v) -\u003e freevars(u, ours) + freevars(v, ours),\n            fun(u, n) -\u003e freevars(u, ours + {n})\n        )\n\n### Example 2\n\nGiven an abstraction term and a value, return a version of\nthe body of the abstraction term where every instance of the\nvariable bound to the abstraction term is replaced by the given\nvalue.  We can call this operation `resolve`.\n\n    let resolve = fun(t, x) -\u003e\n        destruct(t,\n            fun(n) -\u003e t,\n            fun(u, v) -\u003e t,\n            fun(u, n) -\u003e replace_all(u, n, x)\n        )\n    where replace_all = fun(t, m, x) -\u003e\n        destruct(t,\n            fun(n) -\u003e if n == m then x else var(n),\n            fun(u, v) -\u003e app(replace_all(u, m, x), replace_all(v, m, x))\n            fun(u, n) -\u003e abs(n, replace_all(u, m, x))\n        )\n\nNote that this operation was specifically *not* called `subst`,\nbecause the name `subst` is often given to a process that\nreplaces *free* variables, while this operation replaces\n*bound* ones.  It was also specifically not named `beta`\nbecause it does not require that _t_ and _x_ come from the same\napplication term.\n\n### Example 3\n\nThe next task is to write a beta-reducer.  We destruct\nthe term twice, once to ensure it is an application term,\nand again to ensure the application term's first subterm\nis an abstraction term.  Then we use `resolve`, above, to\nplug the application term's second subterm into the\nabstraction term.\n\n    let beta = fun(r) -\u003e\n        destruct(r,\n            fun(n) -\u003e var(n),\n            fun(u, v) -\u003e\n                destruct(u,\n                    fun(_) -\u003e app(u, v),\n                    fun(_, _) -\u003e app(u, v),\n                    fun(u) -\u003e resolve(u, v)\n                ),\n            fun(t) -\u003e t\n        )\n\nIn fact, we _could_ merge this implementation with the\nimplementation of `resolve` and this would save a call\nto `destruct`; but this would be merely an optimisation.\nIt is left as an exercise to any reader who may be so\nmotivated to undertake it.\n\n### Example 4\n\nThe next task would be to search through a lambda term,\nlooking for a candidate application term to reduce, and\nreducing it.  The pseudocode below returns a pair\n`[bool, term]` where the boolean value indicates whether\nthe term has been rewritten by the call or not.  It\nimplements a leftmost-outermost reduction strategy.\n\n    let reduce = fun(t) -\u003e\n        if is_beta_reducible(t) then\n            [true, beta(t)]\n        else\n            destruct(t,\n                fun(n) -\u003e [false, var(n)],\n                fun(u, v) -\u003e\n                    let\n                        [has_rewritten, new_u] = reduce(u)\n                    in\n                        if has_rewritten then\n                            [true, app(new_u, v)]\n                        else\n                            let\n                                [has_rewritten, new_v] = reduce(v)\n                            in\n                                if has_rewritten then\n                                    [true, app(u, new_v)]\n                                else\n                                    [false, app(new_u, new_v)],\n                fun(t) -\u003e [false, t]\n            )\n\nFrom there it's just a hop, a skip, and a jump\nto a proper lambda term normalizer:\n\n    let normalize(t) -\u003e\n        let\n            [has_rewritten, new_t] = reduce(t)\n        in\n            if has_rewritten then\n                normalize(new_t)\n            else\n                t\n\nDiscussion\n----------\n\n### Prior work: Paulson's exercise\n\nThe idea of formulating an ADT for lambda terms is not a new one.\nIn Chapter 9 of \"ML for the Working Programmer\", 1st ed. (1991),\nLawrence Paulson develops an implementation of lambda terms in ML and notes\nthat:\n\n\u003e Signature LAMBDA_NAMELESS is concrete, revealing all the internal\n\u003e details.  [...]  An abstract signature for the λ-calculus would\n\u003e provide operations upon λ-terms themselves, hiding their\n\u003e representation.\n\nSo the idea is an established one; but if so, why does one see so few instances\nof it out in the wild?  I think it's this: most lambda term manipulation code\nsees actual use only academic contexts, most usually in such things as theorem provers.\nThese are contexts that don't greatly benefit from the software\nengineering principle of being able to swap out one implementation\nof an interface with an alternative implementation.  Indeed, in a\ntheorem proving context, an extra level of abstraction may just\nbe another burden that the mechanical reasoning methods need to\ndeal with, with no other benefit.  So concrete data types are used,\nbecause concrete data types are sufficient.\n\nIn the context of Lariat, however, the ADT is the object of study in its own right.\n\nNow, here's the part I elided from the above quoted paragraph:\n\n\u003e Many values of type _term_ are **improper**: they do not correspond to\n\u003e real λ-terms because they contain unmatched bound variable indices.\n\u003e [...]  _abstract_ returns improper terms and _subst_ expects them.\n\nAnd at the end of the section, he poses Exercise 9.16:\n\n\u003e Define a signature for the λ-calculus that hides its internal representation.\n\u003e It should specify predicates to test whether a λ-term is a variable,\n\u003e an abstraction, or an application, and specify functions for abstraction\n\u003e and substitution.\n\nHowever, as he mentioned earlier, these operations produce and expect improper\nterms; so he appears to be asking for an abstract representation of lambda terms\nthat includes improper lambda terms.  [[Footnote 1]](#footnote-1)\nI would argue that such an ADT has a lot less value as an abstraction\nthan an ADT in which only proper lambda terms can be represented.\n[[Footnote 2]](#footnote-2)\n\nAlthough it was not in direct response to this exercise (which I hadn't\nseen for years until I came across it again), it was consideration\nof this point -- how does one formulate an ADT that represents only\nproper lambda terms? -- that led me to formulate Lariat.\n\n### The role of `destruct`\n\n`var`, `app`, and `abs` construct terms, while `destruct` takes them apart.\nConstructing terms is the easy part; it's taking them apart properly that's\nhard.\n\n`destruct` is a \"destructorizer\" in the sense described in \n[this article on Destructorizers](https://codeberg.org/catseye/The-Dossier/src/branch/master/article/Destructorizers/README.md).\nIn fact, this use case of \"taking apart\" lambda terms was one\nof the major motivations for formulating the destructorizer\nconcept.\n\nAlthough it was not specifically intended, `destruct` is also what permits\nthe ADT to be \"total\" in the sense that there are no operations that are\nundefined.\n\n### Equality modulo renaming of bound variables\n\nWhen working with lambda terms, one is often concerned with\ncomparing two lambda terms for equality, modulo renaming of bound\nvariables.  We haven't introduced such an operation because it should\nbe possible to build such an operation using `destruct`; basically,\nrender the two terms as text (or some other concrete representation),\nthen compare the texts for equality.  (This does however require that\nthete is an operation for rendering a name to its textual representation,\nand also that the procedure for obtaining a fresh name is deterministic,\nso that the fresh names generated when `destruct`ing two equal abstractions,\nmatch up in both of the terms.)\n\nOf course, such an operation could be provided as a native\noperation for performance or convenience.  (This is one of the nice\nthings about ADTs -- they can be sub-ADTs of a larger ADT.)  Similarly,\nalthough we have shown that we can implement `freevars` using the operations\nof the ADT, the definition of the `destruct` operation essentially requires\nthat something equivalent to it already exists, and it could be exposed to\nthe user as well.\n\n### The possibility of an algebraic formulation\n\nThe ADT that has been described in this document has been described\nquite precisely (I hope) but not formally.  A direction that this\nwork could be taken in would be to produce a definition of Lariat that\nis actually formal, i.e. in the form of an equational theory, or\nequivalently, an algebra.\n\nThere are reasons to believe this is not impossible.  In\n[The Lambda Calculus is Algebraic](https://www.mscs.dal.ca/~selinger/papers/combinatory.pdf) (PDF)\n(Selinger, 1996) an algebra equivalent to the lambda calculus is\nformed by treating free variables as \"indeterminates\" (although\nI must admit I'm not entirely certain what is meant by that).\nAdditionally, section 1.3 of [Language Prototyping: An Algebraic Specification Approach](https://archive.org/details/languageprototyp0000unse)\n(1996; van Deursen, Heering, Klint eds., borrowable online at archive.org)\ngives a definition of the lambda calculus in the algebraic definition\nlanguage ASF+SDF, which comes fairly close to conventional equational logic\n(although it does contain extras such as conditional equations).\n\nHowever, in Lariat, `destruct` is a \"higher-order\" operation,\nin the sense that it takes functions as parameters, and this may\nwell complicate the task of defining an equational theory based\non Lariat, or it may complicate the resulting equational theory.\nWe'll talk about that in the next section.\n\n### Variation: Partial Lariat\n\nTo support the effort of formulating an algebra based on Lariat,\nor for any other purposes which it may suite, it's worth looking at the possibility\nof replacing `destruct` with a set of \"first-order\" operations.\n\nWhen I first started working out Lariat, I thought that using a\ndestructorizer would be essential to the problem of being able to\ndestruct an abstraction term and have the result be a proper lambda term.\nIt's not as essential as I thought.  What `destruct` does when given\nan abstraction term is, basically, to form a free variable with a\nfresh name (one that does not occur in the abstraction term) and\n`resolve` (as defined in the examples above) the abstraction term with it.\nIf the ADT were to have discrete operations for picking a fresh name\ngiven a lambda term, and for `resolve`, these could be applied\n\"manually\", and in this manner the user could destruct abstraction\nterms just the same as `destruct` does.\n\nThere are subtle differences: `resolve` would need to be an intrinsic operation\nwhich is exposed by the ADT, rather that derived from the basic operations\nof Lariat.  It also gives the user the freedom to apply `resolve` with whatever\nthey wish, while in Lariat, `destruct` can only apply this action with a fresh\nvariable that it itself has chosen, which is significantly more restrictive.\n\nA version of Lariat without `destruct` would also need operations\nfor testing if a term is an abstraction term, vs. an application\nterm, vs. a free variable.  The operation of extracting the first\nand second values from an application term (basically, the theory\nof ordered pairs) would not be sensibly defined for abstraction\nterms or free variables, and so this version of the ADT would be\npartial rather than total, thus the name \"Partial Lariat\".\n\nFootnotes\n---------\n\n#### Footnote 1\n\nEither that or, based on his remark about\nan \"abstract signature for the λ-calculus\", he intended the operations in this\nexercise to be on the level of the lambda calculus, i.e. beta-reduction and\nnormalization?  But that's not what he wrote, and lacking a copy of the 2nd\nedition to see if this has been corrected, I shall take him at his word.\n\n#### Footnote 2\n\nFor more information on this philosophy, see \"Parse, don't Validate\";\nin particular, [LCF-style Natural Deduction](https://codeberg.org/catseye/The-Dossier/src/branch/master/article/LCF-style-Natural-Deduction)\nillustrates how it applies to theorem objects in an LCF-style theorem prover;\nand it applies here too.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatseye%2Flariat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcatseye%2Flariat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatseye%2Flariat/lists"}