{"id":16783223,"url":"https://github.com/ggandor/y-combinator-tutorial","last_synced_at":"2026-01-03T20:04:19.359Z","repository":{"id":110972891,"uuid":"299330331","full_name":"ggandor/y-combinator-tutorial","owner":"ggandor","description":"An extremely short but hopefully enlightening practical explanation of the Y combinator","archived":false,"fork":false,"pushed_at":"2021-06-03T13:31:09.000Z","size":13,"stargazers_count":13,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-23T08:20:28.480Z","etag":null,"topics":["computer-science","lambda-calculus","recursion","y-combinator"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ggandor.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2020-09-28T14:13:35.000Z","updated_at":"2023-12-04T02:25:48.000Z","dependencies_parsed_at":null,"dependency_job_id":"ac683d7c-3564-4d59-8c9b-1a78f3f4cfed","html_url":"https://github.com/ggandor/y-combinator-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggandor%2Fy-combinator-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggandor%2Fy-combinator-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggandor%2Fy-combinator-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggandor%2Fy-combinator-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ggandor","download_url":"https://codeload.github.com/ggandor/y-combinator-tutorial/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243940063,"owners_count":20372044,"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":["computer-science","lambda-calculus","recursion","y-combinator"],"created_at":"2024-10-13T07:49:14.640Z","updated_at":"2026-01-03T20:04:14.325Z","avatar_url":"https://github.com/ggandor.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"Y combinator: A very short explanation\n===\n\nThe following is the most distilled top-down explanation of the Y combinator I\ncould possibly come up with, that - according to my hopes - still remains\ncomprehensible. You might want to read this either as a warm-up before diving\ndeeper, or after reading\n[one](http://blog.tomtung.com/2012/10/yet-another-y-combinator-tutorial/) or\n[two](https://www.cs.toronto.edu/~david/courses/csc324_w15/extra/ycomb.html)\nmore verbose tutorials with a bottom-up approach, if the concept hasn't quite\nclicked yet.\n\nCaveat: being short does not mean that you can read faster than usual - in fact,\nexactly the opposite. The text is very dense, and every sentence is packed with\na lot of information: be sure to take your time.\n\nThe code itself is in [Clojure](https://clojure.org/), a popular modern Lisp\ndialect - the only thing that should not be instantly comprehensible for Lispers\nis the following built-in macro: `#(... %)` is an alternative syntax for\ndenoting a lambda, the same as `(fn [x] (... x))` - Clojure's equivalent of\n`(lambda (x) (... x))` -, with `%` representing the place of the first or only\nfunction argument in the body.  I will use that form to denote those lambdas\nthat are really just redundant wrappers, serving no other purpose than to delay\nthe evaluation of their wrapped expression.\n\nProblem\n---\nWe have an anonymous function `f` that we'd like to be able to call recursively,\nwithout the means of explicit self-reference.\n\n```clojure\n(def f (fn [x] (\n         ;; point of recursion somewhere in body\n       )))\n```\n\nSolution\n---\nThe **Y combinator** is a clever function that can create a modified version of\n`f` that is able to recurse without knowing its own name. I will first show the\nso-called strict version of the Y combinator, commonly called the **Z\ncombinator** - the subtle difference will be addressed later. \n\nAs a preliminary step, we need to move the self-reference out from the function\n`f`. That is, the Z combinator does not work on the original function, but\nexpects a wrapped version of `f` (a maker for `f`, if you like), that returns an\n`f` that calls the maker's _argument_ - a bound variable in the enclosing scope,\nthat's perfectly OK - at the point of recursion.\n\n```clojure\n(def f-maker (fn [f-self]\n               ;; Spoiler: the function returned here will be a clever,\n               ;; \"self-replicating\" version of our original `f`, if\n               ;; provided with the right form of `f-self` by the maker.\n               (fn [x] (\n                 ;; call the provided `f-self` at the point of recursion\n               ))))\n```\n\nNow the trick waiting to be performed by us is passing this new kind of `f`\n_itself_ as argument to `f-maker` somehow. On how that can be achieved in an\nindefinite recursive situation, without manually feeding `f-maker` with an `f`\nthat is created by an `f-maker` taking an `f` that is created by... and so on,\nthat is, writing out an unfinishable chain of nested calls, see the rest.\n\nWithout further ado, the magic function:\n\n```clojure\n(def Z (fn [f-maker]\n         ((fn [self] (f-maker #((self self) %)))\n          (fn [self] (f-maker #((self self) %))))))\n```\n\nA helper function that might make it easier to read: `self-apply` (alias U\ncombinator) is simply `(fn [x] (x x))`.\n\n```clojure\n(def Z (fn [f-maker]\n         (self-apply\n           (fn [self] (f-maker #((self-apply self) %))))))\n```\n\nThe important thing to note above is that the `(self-apply self)` form inside,\nwhen called, will expand to the body of `Z` itself, _the very form with which we\nhave started_. That is, `Z` calls `f-maker` with a box containing `Z`'s own\nbody, and thus _loads it into the resulting function_. This is a - probably the - \nkey step to understand here, the rest follows pretty trivially. If the \"why\"\nis not clear yet, just walk through the substitutions step by step; it is a\ncrucial exercise.\n\nIn the end, this results in getting a version of our original `f` (let it be\n`self-replicating-f`) that has the same body as `f`, except that it has the\nmeans to recreate itself on demand: at the point of recursion, it has the body\nof `Z`_, with_ `f-maker` _already bound_, boxed in, ready to be evaluated.\n\nOnce that happens, i.e., when a recursive call is initiated in our supercharged\n`self-replicating-f`, the box opens, and the whole machinery inside starts\nmoving again, with the body of `Z` - via calling `f-maker` - ultimately reducing\nitself to the same `self-replicating-f`, that is finally ready to take its\nargument (the one being passed in the recursive call) and execute.\n\n```clojure\n;; self-replicating-f\n(fn [x] (\n  ;; call `#((self-apply self) %)` i.e. `#(Z-body-with-f-maker-enclosed %)`\n  ;; i.e. `#(self-replicating-f %)` at the point of recursion\n))\n```\n\nAnd that's pretty much it.\n\n```clojure\n(def self-replicating-f (Z f-maker))\n```\n\nThe Y combinator\n---\nThe Y combinator is the same as the Z combinator, except that it does not wrap\nthe `(self-apply self)` call in a lambda, conveniently delaying evaluation. This\nonly works in lazy languages though, like Haskell, where functions evaluate\ntheir arguments only when needed, and not before executing the body. Otherwise\nwe'd be stuck in an infinite recursion - `(self-apply self)` would expand\nforever after the first call, before it could be passed on to `f-maker`.\n\n```clojure\n(def Y (fn [f-maker]\n         (self-apply\n           (fn [self] (f-maker (self-apply self))))))\n```\n\nTODO\n---\n- [ ] Go through and summarize [McAdam's\n  paper](http://www.lfcs.inf.ed.ac.uk/reports/97/ECS-LFCS-97-375/) on the\n  practical applications of the concept.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fggandor%2Fy-combinator-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fggandor%2Fy-combinator-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fggandor%2Fy-combinator-tutorial/lists"}