module Realisability.PCA where

Partial combinatory algebras🔗

A partial combinatory algebra (PCA) is a set equipped with enough structure to model universal computation: a partial application operator1 and a notion of abstraction elimination, which will be defined below. This structure is enough for to model a simple programming language in the spirit of the untyped lambda calculus.

While the definition of PCA is austere, adapting traditional encoding techniques from untyped lambda calculus lets us show that PCAs support encoding booleans, pairs, sums, and even primitive recursion.

Note

Formalising the partial application operator is slightly tricky. In the literature (e.g. following de Jong (2024)) one would imagine that the application operator on has type

  _ = 𝔸  𝔸   𝔸

i.e., that it takes two defined elements of and returns a partial element. However, working with an operator of this type is rather cumbersome, because we can’t form iterated expressions like the application lives in not and so can not be the left operand in an application. Instead, we define application to work over partial elements, i.e. the type of our application operators is

  PAS : Type 
  PAS =  𝔸   𝔸   𝔸

The inhabitants of a PCA🔗

If is a partial combinatory algebra (or, more generally, a partial applicative structure), the inhabitants of the type often serve dual purposes: they can be values or they can be programs. Of course, since PCAs implement a simple higher-order programming language, programs are a type of value. However, there are still situations where it is important to be clear which of these two roles a given is serving.

Applying a program to a given value may not result in a value— execution could diverge, for example. We think of an arbitrary application like as a computation, which may or may not produce a value— if it does produce a value, then it comes from exactly one value in

If is a partial combinatory algebra, we refer to the type as the type of values in , or programs in , depending on the role that each inhabitant is playing.

We also refer to the type as the type of computations in .

Abstraction elimination🔗

The type of terms over with free variables is defined inductively: A term is either one of the variables, a value drawn from or the application of a term to another.

data Term (A : Type ) (n : Nat) : Type (level-of A) where
  var   : Fin n  Term A n
  const : ↯⁺ A  Term A n
  app   : Term A n  Term A n  Term A n

If we are given a term in variables and an environment, a list of values, we can define the evaluation of a term to be the computation obtained by looking each variable up in the environment, preserving embedded values, and using the partial applicative structure to interpret the app constructor. Moreover, if we have a term in variables and a term in variables, we can instantiate with to obtain a term in variables, where the zeroth variable used in has been replaced with throughout.

module eval (_%_ : PAS A) where
  eval : Term A n  Vec (↯⁺ A) n   A
  eval (var x)   ρ = lookup ρ x .fst
  eval (const x) ρ = x .fst
  eval (app f x) ρ = eval f ρ % eval x ρ

  inst : Term A (suc n)  Term A n  Term A n
  inst (var x) a with fin-view x
  ... | zero = a
  ... | suc i = var i
  inst (const a) _ = const a
  inst (app f x) a = app (inst f a) (inst x a)

These two operations are connected by the following lemma: evaluating a term instantiated with a value is the same as evaluating the original term in an extended environment.

  abstract
    eval-inst
      : (t : Term A (suc n)) (x : ↯⁺ A) (ρ : Vec (↯⁺ A) n)
       eval (inst t (const x)) ρ  eval t (x  ρ)
    eval-inst (var i) y ρ with fin-view i
    ... | zero  = refl
    ... | suc j = refl
    eval-inst (const a) y ρ = refl
    eval-inst (app f x) y ρ = ap₂ _%_ (eval-inst f y ρ) (eval-inst x y ρ)

A partial applicative structure is a partial combinatory algebra when we have an operation abs sending terms in variables to terms in variables which behave, under evaluation, as “functions of ”. In particular, functions should always be defined values (abs↓), and we have a restricted law (abs-β) saying that applying a function to a value should be the same as evaluating the body of the function instantiated with that value, or equivalently in an environment extended with that value.

record is-pca (_%_ : PAS A) : Type (level-of A) where
  open eval _%_ public
  field
    abs   : Term A (suc n)  Term A n
    abs↓  : (t : Term A (suc n)) (ρ : Vec (↯⁺ A) n)   eval (abs t) ρ 
    abs-β : (t : Term A (suc n)) (ρ : Vec (↯⁺ A) n) (a : ↯⁺ A)
           eval (abs t) ρ % a .fst  eval (inst t (const a)) ρ
\ Warning

Traditional texts on realisability (e.g. de Jong op. cit., see also Bauer (2022)) define partial combinatory algebras in terms of combinators, typically and These can be defined using the abstraction operator in the typical way, e.g.

We prefer taking the abstraction elimination procedure as a primitive since it works better with Agda’s type inference, in particular when working against an arbitrary PCA— but a combinatorially complete partial applicative structure is a PCA in our sense.

In the formalisation we often define and apply functions to many arguments, so we define an version of the abstraction combinator and of the law.
  absₙ : (k : Nat)  Term A (k + n)  Term A n
  absₙ zero    e = e
  absₙ (suc k) e = absₙ k (abs e)

  _%ₙ_ :  {n}   A  Vec (↯⁺ A) n   A
  a %ₙ []       = a
  a %ₙ (b  bs) = (a %ₙ bs) % b .fst

  abstract
    abs-βₙ
      : {k n : Nat} {e : Term A (k + n)}
       (ρ : Vec (↯⁺ A) n) (as : Vec (↯⁺ A) k)
       (eval (absₙ k e) ρ %ₙ as)  eval e (as ++ ρ)
    abs-βₙ ρ [] = refl
    abs-βₙ {e = e} ρ (x  as) = ap (_% x .fst) (abs-βₙ ρ as)  abs-β _ (as ++ ρ) x  eval-inst e x (as ++ ρ)

  1. A set equipped with only the application operator, and not necessarily with an abstraction elimination procedure, will be referred to as a partial applicative structure, or PAS.↩︎


References