Definition: Tree Calculus expressions E have abstract syntax E ::= Δ | E E and are reduced according to the following small-step semantics:

Δ Δ a ba(1)Δ (Δ a) b ca c (b c)(2)Δ (Δ a b) c Δa(3a)Δ (Δ a b) c (Δ u)b u(3b)Δ (Δ a b) c (Δ u v)c u v(3c)

Notation: Binary operator is left associative, unless overridden using parentheses.


Conventions:

  • We call irreducible expressions “values”.
  • We can think of expressions as unlabeled trees, generated by interpreting every subexpression (Δ arg  arg) as a node with arg  arg as child trees. Note that this is not the same as the ASTs that arise from the abstract syntax above (they would have inner nodes that represent the E E “function application” operator, while the trees we propose have inner/all nodes correspond to one Δ each).
    • We call expression Δ “leaf”.
    • We call expressions Δ a “stem of a”.
    • We call expressions Δ a b “fork of a and b”.


Tip: Check out Timur Latypoff’s visualization of these rules and conventions. Thanks Timur!


Consequences:

  • Values are binary trees, i.e. only consist of leafs, stems and forks: As long as there is a subexpression (Δ a b c), one of the reduction rules apply.
  • Tree Calculus is Turing complete: Reduction rules 1 and 2 are analogous to K and S combinators.
  • Tree Calculus is intensional: Rules 3a-c allow reflecting on values by distinguishing leafs, stems and forks.
  • Tree Calculus is confluent: The five reduction rules are non-overlapping and only allow observing values, not reducible expressions.


For significantly more detail and discussion, read Reflective Programs in Tree Calculus by Barry Jay. (Note: the reduction rules above are a revised version of the ones presented in the book.)


Example reduction: Negating booleans

If we define false = Δ, true = Δ Δ and not = Δ (Δ (Δ Δ) (Δ Δ Δ)) Δ then

not false=defΔ (Δ (Δ Δ) (Δ Δ Δ)) Δ Δ3aΔ Δ=deftrue

and

not true=defΔ (Δ (Δ Δ) (Δ Δ Δ)) Δ (Δ Δ)3bΔ Δ Δ Δ1Δ=deffalse

The tree representation of not is



  • Basic implementation in OCaml

    Try this at try.ocamlpro.com

    (* TC       |  OCaml
     * ---------+-------------
     * t        |  Leaf
     * t a      |  Stem a
     * t a b    |  Fork (a, b)
     *)
    
    type t =
      | Leaf
      | Stem of t
      | Fork of t * t
    
    let rec apply a b =
      match a with
      | Leaf -> Stem b
      | Stem a -> Fork (a, b)
      | Fork (Leaf, a) -> a
      | Fork (Stem a1, a2) -> apply (apply a1 b) (apply a2 b)
      | Fork (Fork (a1, a2), a3) ->
        match b with
        | Leaf -> a1
        | Stem u -> apply a2 u
        | Fork (u, v) -> apply (apply a3 u) v
    
    (* Example: Negating booleans *)
    let _false = Leaf
    let _true = Stem Leaf
    let _not = Fork (Fork (_true, Fork (Leaf, _false)), Leaf)
    apply _not _false (* Stem Leaf = _true  *)
    apply _not _true  (* Leaf      = _false *)
    
  • More efficient implementation in JavaScript

    Try this at runjs.app

    // TC       |  JS
    // ---------+-----------
    // t        |  []
    // t a      |  [a]
    // t a b    |  [b, a]
    // t a b c  |  [c, b, a]
    
    const apply = (a, b) => {
      const expression = [b, ...a]
      const todo = [expression];
      while (todo.length) {
        const f = todo.pop();
        if (f.length < 3) continue;
        todo.push(f);
        const a = f.pop(), b = f.pop(), c = f.pop();
        if (a.length === 0) f.push(...b);
        else if (a.length === 1) {
          const newPotRedex = [c, ...b];
          f.push(newPotRedex, c, ...a[0]);
          todo.push(newPotRedex);
        }
        else if (a.length === 2)
          if (c.length === 0) f.push(...a[1]);
          else if (c.length === 1) f.push(c[0], ...a[0]);
          else if (c.length === 2) f.push(c[0], c[1], ...b);
      }
      return expression;
    };
    
    // Example: Negating booleans
    const _false = [];
    const _true = [[]];
    const _not = [[],[[_false,[]],_true]];
    apply(_not, _false); // [[]] = _true
    apply(_not, _true);  // []   = _false