{"id":16774968,"url":"https://github.com/ngzhian/ski","last_synced_at":"2025-06-30T20:07:08.006Z","repository":{"id":148405265,"uuid":"91074605","full_name":"ngzhian/ski","owner":"ngzhian","description":"SKI combinators","archived":false,"fork":false,"pushed_at":"2017-05-27T11:30:27.000Z","size":20,"stargazers_count":54,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-04T02:01:31.292Z","etag":null,"topics":["lambda-calculus","ski-combinator-calculus","ski-expression"],"latest_commit_sha":null,"homepage":null,"language":"OCaml","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/ngzhian.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":"2017-05-12T09:25:13.000Z","updated_at":"2025-02-23T17:50:25.000Z","dependencies_parsed_at":null,"dependency_job_id":"c7d3a865-2801-4ca5-bde7-d69a6a61cdb7","html_url":"https://github.com/ngzhian/ski","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ngzhian/ski","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngzhian%2Fski","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngzhian%2Fski/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngzhian%2Fski/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngzhian%2Fski/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ngzhian","download_url":"https://codeload.github.com/ngzhian/ski/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngzhian%2Fski/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262842921,"owners_count":23373167,"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":["lambda-calculus","ski-combinator-calculus","ski-expression"],"created_at":"2024-10-13T06:50:38.732Z","updated_at":"2025-06-30T20:07:05.532Z","avatar_url":"https://github.com/ngzhian.png","language":"OCaml","readme":"An experiment in SKI combinator calculus, below are the accompanying blog posts,\noriginally on [my blog](https://blog.ngzhian.com):\n\n1. [SKI combinators - AST and Evaluating](https://blog.ngzhian.com/ski.html)\n2. [SKI combinators - Lambda to SKI](https://blog.ngzhian.com/ski2.html)\n\n---\n\n# SKI combinators - AST and Evaluating\n\nS, K, and I are the name of three combinators.\nPerhaps surprisingly, these combinators together is sufficient to\nform a Turing-complete language [^1],\nalbeit very tedious to write.\nAny expression in lambda calculus can be translated into the SKI combinator calculus via\na process called *abstraction elimination*, and that is what this post will be exploring.\n\n[^1]: Wikipedia [Combinatory Logic](https://en.wikipedia.org/wiki/Combinatory_logic#Completeness_of_the_S-K_basis)\n\n## The SKI combinators\n\nCombinators are functions without free variables, for example, in pseudo-Haskell syntax:\n`id x = x` is a combinator that returns it's argument unchanged.\nAnd here is what the SKI combinators do:\n\n```\nI x     = x\nK x y   = x\nS x y z = x z (y z)\n```\n\nI is the identity function, K behaves like a two argument identity function, returning\nthe first argument passed to it, S performs substitution (function application).\nHere are some examples:\n\n```\nI I = I\nK K I = K\nS K S K = K K (S K) = K\n```\n\n## SKI abstract syntax tree\n\nTo be more precise, the SKI combinator calculus is made up of terms:\n1. S, K, and I are terms\n2. `(x y)` are terms if `x` and `y` are terms\n\nThus an expression in the SKI system can be visualized as a binary tree,\neach leaf node being S, K, or I, and an inner node representing the parentheses.\n\nLet's define an abstract syntax tree for an SKI expression like so:\n\n\n```ocaml\ntype ski =\n  | I\n  | K\n  | S\n  | T of ski * ski\n```\n\nThus the terms used in the examples above can be constructed as such:\n\n```ocaml\nT (I, I)               (* I I     *)\nT (T (K, K), I)        (* K K I   *)\nT (T (T (S, K), S), K) (* S K S K *)\n```\n\nWith the abstract syntax tree, we can now try to reduce or interpret the SKI expressions.\nWe will be looking at two different ways of doing so, the first is by\ninterpreting recursively, the second is by running it as a stack machine.\n\n## Interpreting\n\nWe interpret expressions by pattern matching on the structure of the abstract syntax tree:\n\n```ocaml\nlet rec interp c =\n  match c with\n  (* leaf node, remain unchanged *)\n  | I | K | S              -\u003e c\n  (* an I term, reduce argument *)\n  | T (I, x)               -\u003e interp x\n  (* a K term, reduce first argument *)\n  | T (T (K, x), y)        -\u003e interp x\n  (* an S term, perform substitution *)\n  | T (T (T (S, x), y), z) -\u003e\n    interp (T (T (x, z), T (y, z)))\n  (* any other term *)\n  (* the goal here is to check if terms are reducible *)\n  (* to prevent infinite recursion   *)\n  | T (c1, c2)             -\u003e\n    let c1' = interp c1 in\n    let c2' = interp c2 in\n    if c1 = c1' \u0026\u0026 c2 = c2'\n    then T (c1, c2)\n    else interp (T (c1', c2'))\n```\n\n## Stack machine\n\nThe idea for a stack machine based reduction of the SKI calculus is from [^2]\n\n[^2]:http://yager.io/HaSKI/HaSKI.html\n\nFirst we define a step function for the machine,\nwhich works on the current term, and updates the stack based on the calculus rules.\n\n```ocaml\ntype step =\n  (* able to perform next step with term and current stack *)\n  | Step of (ski * ski list)\n  (* no reduction possible anymore *)\n  | End of ski\n\nlet step term stack =\n  match (term, stack) with\n  (* I term, work on the top term in the stack *)\n  | I, x::s -\u003e Step(x , s)\n  (* K term, work on the top term, discard the second *)\n  | K, x::y::s -\u003e Step(x, s)\n  (* works on the substituted term *)\n  | S, x::y::z::s -\u003e\n    Step(T (T (x, z), T(y, z)), s)\n  (* push the second pargument onto the stack *)\n  | T (c1, c2), s -\u003e Step(c1, c2 :: s)\n  (* empty stack, return as the result of reduction *)\n  | e, [] -\u003e End e\n  (* no idea how to handle this *)\n  | _ -\u003e failwith \"Unrecognized term\"\n```\n\nThen a full run of an expression will be running the step function until we hit the end:\n\n```ocaml\nlet run term =\n  let rec go term stack =\n    match step term stack with\n    | End e -\u003e e\n    | Step (e, s') -\u003e go e s'\n  in\n  go term []\n```\n\nNext up: translating terms in lambda calculus to SKI combinators.\n\n## References\n\n1. Wikipedia [SKI Combinator calculus](https://en.wikipedia.org/wiki/SKI_combinator_calculus)\n2. Wikipedia [Combinatory Logic](https://en.wikipedia.org/wiki/Combinatory_logic)\n3. [The SKI Combinator Calculus a universal formal system](http://people.cs.uchicago.edu/~odonnell/Teacher/Lectures/Formal_Organization_of_Knowledge/Examples/combinator_calculus/)\n4. [eperdew's implementation in OCaml](https://github.com/eperdew/SKI/)\n5. [bmjames' implementation in Erlang](https://gist.github.com/bmjames/745291/ab52ffdf8230bbec9bcf1059825ad6d3fd7186f5)\n6. [yager's implementation in Haskell](http://yager.io/HaSKI/HaSKI.html)\n7. [ac1235's implementation in Haskell's happy](https://github.com/ac1235/ski-combinator-calculus/blob/master/ski.y)\n\n---\n\n# SKI combinators - Lambda to SKI\n\nIn a [previous post](./ski.html),\nwe looked at what SKI combinators are, and how to encode and interpret them.\nWe also mentioned that these 3 combinators form a Turing-complete language,\nbecause every lambda calculus term can be translated into an SKI combinator term.\n\n\u003e Source code is available [here](https://github.com/ngzhian/ski)\n\n## Lambda Calculus\n\nThe [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)\nis a simple Turing-complete language.\n\n[^1]: Wikipedia [Combinatory Logic](https://en.wikipedia.org/wiki/Combinatory_logic#Completeness_of_the_S-K_basis)\n\n\nLambda calculus is made up of three terms:\n\n1. Variable, such as `x`,\n2. Lambda abstraction, such as `fun x -\u003e x`,\n3. Application, such as `(y x)`.\n\n```ocaml\n(* lambda calculus AST *)\ntype name = string\ntype lambda =\n  | Var of name\n  | App of lambda * lambda\n  | Abs of name * lambda\n```\n\nHere's an example lambda term,\nrepresenting the application of an identity function to an identity function:\n\n```ocaml\nApp (Abs ('x', Var 'x'), Abs ('y', Var 'y'))\n```\n\n## Translating lambda to SKI\n\nLet us conjure an ideal function that will do such a translation,\nit should take a lambda term to an SKI term:\n\n```ocaml\nlet convert (e : lambda) : ski = (??)\n```\n\nWhat this means is that we can either have a lambda term, or an ski term, with no in-betweens,\ni.e. we cannot have a lambda term containing an ski term.\n\nHowever, if we look at the translation rules,\nwe find that we will need a intermediate structure that can hold both lambda terms\nand ski terms.\n\nFor example in clause 5, `T[λx.λy.E] =\u003e T[λx.T[λy.E]]`,\nthe translated term `T[λy.E]`, which by definition is an SKI term,\nis the body of a lambda abstraction.\n\nSo it is helpful to define such a structure,\nwhich allows lambda calculus terms and SKI terms to coexist:\n\n```ocaml\n(* Intermediate AST for converting lambda calculus into SKI combinators.\n * This is needed because when converting, intermediate terms can be\n * a mixture of both lambda terms and SKI terms, for example\n * a lambda expression with a SKI body, \\x . K\n * *)\ntype ls =\n  | Var of name\n  | App of ls * ls\n  | Abs of name * ls\n  | Sl\n  | Kl\n  | Il\n  | Tl of ls * ls\n\n(* String representation of ls *)\nlet rec string_of_ls (l : ls) : string = match l with\n    | Var x -\u003e x\n    | App (e1, e2) -\u003e \"(\" ^ (string_of_ls e1) ^ (string_of_ls e2) ^ \")\"\n    | Abs (x, e) -\u003e \"\\\\\" ^ x ^ (string_of_ls e)\n    | Sl  -\u003e \"S\"\n    | Kl  -\u003e \"K\"\n    | Il  -\u003e \"I\"\n    | Tl (e1, e2) -\u003e  \"(T \" ^ (string_of_ls e1) ^ (string_of_ls e2) ^ \")\"\n```\n\nWe will also need a helper function to determine if a variable is free in an expression:\n\n```ocaml\n(* Is x free in the expression e? *)\nlet free x (e : ls) =\n  (* Get free variables of an expression *)\n  let rec fv (e : ls) = match e with\n    | Var x -\u003e [x]\n    | App (e1, e2) -\u003e fv e1 @ fv e2\n    | Abs (x, e) -\u003e List.filter (fun v -\u003e v != x) (fv e)\n    | Tl (e1, e2) -\u003e fv e1 @ fv e2\n    | _ -\u003e []\n  in\n  List.mem x (fv e)\n```\n\nThe core translation algorithm then follows the translation scheme\ndescribed in the\n[Wikipedia article](https://en.wikipedia.org/wiki/Combinatory_logic#Completeness_of_the_S-K_basis).\nWe make use of the intermediate structure, `ls`, when translating.\nThe signature of this structure doesn't say much, it looks like an identity function,\nbut the assumption is that the input term is converted from a lambda term,\nmade up of `Var`, `App`, and `Abs`, and the output term will only contain\n`Sl`, `Kl`, `Il`, and `Tl`, i.e. the terms that can be converted\ndirectly into the SKI combinators.\n\n```ocaml\n(* This is the core algorithm to translate ls terms (made up of lambda)\n * into ls terms (made up of SKI combinators).\n * The clauses described here follows the rules of the T function described at\n * https://en.wikipedia.org/wiki/Combinatory_logic#Completeness_of_the_S-K_basis\n * *)\nlet rec translate (e : ls) : ls = match e with\n  (* clause 1. *)\n  (* you can't do much with a variable *)\n  | Var x -\u003e\n    Var x\n  (* clause 2. *)\n  (* an application remains an application, but with the terms translated *)\n  | App (e1, e2) -\u003e\n    App (translate e1, translate e2)\n  (* clause 3. *)\n  (* when x is not free in e, there can be two cases:\n   * 1. x does not appear in e at all,\n   * 2. x appears bound in e, Abs (x, e') is in e\n   * In both cases, whatever you apply this lambda term to will not affect\n   * the result of application:\n   * 1. since x is not used, you can ignore it\n   * 2. the x is bound to an inner argument, so it's really a different x from this\n   * hence this is really a constant term e,\n   * which is the same as the K combinator with e as the first argument.\n   * (recall that: K x y = x) *)\n  | Abs (x, e) when not (free x e) -\u003e\n    App (Kl, translate e)\n  (* clause 4. *)\n  | Abs (x, Var x') -\u003e\n    (* this is the identity function, which is the I combinator *)\n    if x = x'\n    then Il\n    (* we will never hit this case because, when x != x',\n     * we end up in clause 3, as x is not free in Var x' *)\n    else failwith \"error\"\n  (* clause 5. *)\n  | Abs (x, Abs (y, e)) -\u003e\n    (* when x is free in e, the x in e is the argument,\n     * we first translate the body into a combinator, to eliminate a layer of abstraction *)\n    if free x e\n    then translate (Abs (x, translate (Abs (y, e))))\n    else failwith \"error\"\n  (* clause 6. *)\n  | Abs (x, App (e1, e2)) -\u003e\n    (* eliminate the abstraction via application *)\n    (* Recall that S x y z = x z (y z),\n     * so applying the term Abs (x, App (e1, e2)) to an argument x\n     * will result in substituting x into the body of e1, x z,\n     * and e2, y z, and applying e1 to e2, x z (y z) *)\n    if free x e1 || free x e2\n    then App (App (Sl, (translate (Abs (x, e1)))), translate (Abs (x, e2)))\n    else failwith \"error\"\n  | Kl -\u003e Kl\n  | Sl -\u003e Sl\n  | Il -\u003e Il\n  | _ -\u003e\n    failwith (\"no matches for \" ^ (string_of_ls e))\n```\n\nFinally we can write the top level `convert` function we imagined earlier:\n\n```ocaml\n(* Converts a lambda term into an SKI term *)\nlet convert (e : lambda) : ski =\n  (* Convert lambda term into intermediate ls term *)\n  let rec ls_of_lambda (e : lambda) =\n    match e with\n    | Var x -\u003e Var x\n    | App (e1, e2) -\u003e App (ls_of_lambda e1, ls_of_lambda e2)\n    | Abs (x, e) -\u003e Abs (x, ls_of_lambda e)\n  in\n  (* Convert intermediate ls term into ski term *)\n  let rec ski_of_ls (e : ls) : ski =\n    match e with\n    | Var _ -\u003e failwith \"should not have Var anymore\"\n    | Abs _ -\u003e failwith \"should not have Abs anymore\"\n    | App (e1, e2) -\u003e T (ski_of_ls e1, ski_of_ls e2)\n    | Sl  -\u003e S\n    | Kl  -\u003e K\n    | Il  -\u003e I\n    | Tl (e1, e2) -\u003e T (ski_of_ls e1, ski_of_ls e2)\n  in\n  (* convert lambda term into ls term *)\n  let ls_term = ls_of_lambda e in\n  (* translate ls term of lambda into ls term of combinators *)\n  let ls_comb = translate ls_term in\n  (* convert ls term into ski *)\n  ski_of_ls ls_comb\n```\n\nLet's try it with the example given by Wikipedia:\n\n```ocaml\n(* Example lambda terms *)\nlet l2 : lambda = Abs (\"x\", Abs (\"y\", App (Var \"y\", Var \"x\")))\n\nlet _ = print_endline (string_of_ski (convert l2))\n```\n\nThe output `T(T(S,T(K,T(S,I))),T(T(S,T(K,K)),I))`, is the same as `(S (K (S I)) (S (K K) I))`.\n\n## References\n\n1. Wikipedia [SKI Combinator calculus](https://en.wikipedia.org/wiki/SKI_combinator_calculus)\n2. Wikipedia [Combinatory Logic](https://en.wikipedia.org/wiki/Combinatory_logic)\n3. Wikipedia [Lambda Calculus](https://en.wikipedia.org/wiki/Lambda_calculus#Free_variables)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngzhian%2Fski","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fngzhian%2Fski","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngzhian%2Fski/lists"}