Introduction to EA Type Theory

Whys, installation, simple types/programs/proofs

Edit: very outdated, access https://github.com/moonad/formality

Formality is a “proofgramming” language aiming to be fast (non garbage-collected, parallelizable, beta-optimal) and simple (with a small specification that can be implemented independently). The type theory behind Formality is called EA-TT. Here, I’ll explain how to install EA-TT and how to port existing Agda code to equivalent proofs on this core.

1. Installing

At the moment of this writing, EA-TT is implemented in JavaScript. This choice was made because it is the most wide-spread, cross-platform language. Implementing it in other places should take no more than a few days of work. If you’re willing to do so, please contact me!

To install it, you must have npm. Once you do, just type:

npm i -g formality-lang

And that’s it. Create a intro.fm file with the following contents:

.main (hello world)

And test it with formality -n main (on the same directory). If it outputs:

(hello world)

Then you’re good to go. Optionally, you can also use it as a JS library or, if you don’t want to download anything, you can try our online editor here.

2. Untyped language overview

2a. Definitions, lambdas and applications

I’ll start by introducing Formality’s untyped primitives. Its main one is the lambda. It works almost like Agda functions. Here is its syntax:

pri | Formality | JavaScript | Agda

--- | -------------- | ------------------ | ----------------

def | .x val | x = val | x = val

lam | [x] body | (x) => body | λx → body

app | (f x) | f(x) | f x

box | | val | [val] | (box val)

dup | [x = val] body | (x = val[0]), body | let (box x) = body

Let’s test it. Save this program as intro.fm :

.id [x] x

.main (id 42)

Then run it with formality -n main . This command reads all .fm files on the current directory and loads the definitions inside them. The -n main bit means it must eval the main definition and output the result. It’ll print 42 , which is the result of (id 42) . Note that, here, 42 is not an actual number, but an undefined reference. We’ll give meaning to it later.

2b. Strong normalization, explicit duplications

There is one difference between Formality and Agda lambdas. Like Agda, Formality is strongly normalizing language, which means all its programs halt. This is desirable in proof languages because non-terminating programs often allow you to prove logical absurds, “breaking” them. In Agda, this is enforced by a “termination checker” which forbid, for example, non-structural recursion. In Formality, the restriction is much stronger: only affine functions are allowed! This means lambda-bound variables can only appear, at most, once. This, for example, isn’t allowed:

.copy [x] [t] (t x x)

.main (copy foo)

(Note that, since we’re not dealing with types yet, running this program with formality -n main actually works (and outputs [t] (t foo foo) ), but you won’t be able to assign a type to it.) That restriction makes the language obviously terminating, but also very limited. To amend this problem, we introduce new primitives for explicit, “boxed” duplications, put and dup :

pri | Formality | JavaScript | Agda

--- | -------------- | ------------------ | ----------------

def | .x val | x = val | x = val

lam | [x] body | (x) => body | λx → body

app | (f x) | f(x) | f x

put | | val | [val] | (put val)

dup | [x = val] body | (x = val[0]), body | (λ{(put x) → body} val)

Here, put is equivalent to this Agda constructor:

data Box (A : Set) : Set where

put : A → Box

This primitive allows us to:

Put a value “inside a box” with a pipe ( |value ). Copy a boxed value with [x = value] body , consuming the box.

With a few caveats:

Copy-bound variables must have 1 box between definition and occurrence. Boxed values can’t reference variables bound by outer lambdas.

The cool thing about explicit, boxed duplications is that, with those simple restrictions, we’re able to avoid infinite loops, while, at the same time, keeping the language extremely expressive. Technically, it captures all the programs in O(tow(n,s)) , where tow(n,s) = 2^2^2...s (n times) . Later on, I’ll explain how to encode for-loops and arbitrary bounded recursion using those primitives. For now, let’s test what we have so far.

Save the following program as intro.fm :

.id [x] x

.main [val = id] (val val)

This program makes a copy of id and applies it to itself. The result should be just id , i.e., [x] x , but, if we run it with formality -n main , it outputs:

[val = [x] x] (val val)

As you can see, the copy doesn’t happen. That’s because id wasn’t boxed. We can fix it by putting it in a box with a pipe ( | ):

.main [val = | id] (val val)

And now this works as expected! But this is still not legal, though, because there isn’t exactly 1 box between where val is bound and where it is used. This can be easily fixed:

.id [x] x

.main [val = |id] | (val val)

And that’s it. Those are all the native features of the untyped fragment of Formality. Lambdas, applications, boxes and duplications. This language is interesting for many reasons. It is terminating even without types. It is compatible with massively parallel runtimes. It can be evaluated without garbage collection. And so on. If you want to explore more of our untyped fragment, please check this repository, which also includes a proof sketch on the normalization argument. A very insightful exercise must be to follow a standard lambda calculus tutorial, except using that language instead.

3. Type system overview

Dependent type systems are the perfect example on how some things can be technically very simple, while at the same time being “difficult” for humans to understand. Formality’s type system is very small, similarly to the Calculus of Constructions, yet very powerful. If you’re familiar with Agda, though, it should be pretty easy to understand. Here is an overview:

3a. Forall

The first type to understand is the type of a function, {x : A} B . It can be read as “a function that takes an input of type A and returns an output of type B ”. It is almost equivalent to, for example, Haskell’s function type, A -> B , with one caveat: its return type may refer to its argument, x .

3b. Box

The second type is the type of a box, !A . This can be read as “ A is a boxed value of type A" . Boxes are, essentially, pairs of one element, with a native operation to duplicate their contents. There isn’t much else to say about them.

3c. Type

The third type is the type of types, written as Type . As in Agda, in Formality, types are first-class values: you can use them inside your programs, return them from functions, make lists of them and so on. The type of a type is always Type , so, the type of {x : A} B is Type , the type of !A is Type , and the type of Type is itself is also Type . In most proof languages, this would be problematic. Since Formality’s termination doesn’t rely on types, we can actually have that without introducing non-halting computations.

3d. Self

The last type is the self, written as $x A . This is the type of types that can refer to their own values. It also requires two new terms to introduce and use it, @typ val and ~val respectively. Self types are essential to construct inductive datatypes, in a way that will elaborated later on.

3e. Erased

Formality also includes “erased lambdas”, which are literally erased after type-checking. They’re useful for making polymorphic functions without extra runtime costs. Also, due to Formality’s linearity, they are important for our recursive datatype encodings, since erased functions aren’t affected by the restrictions imposed by the explicit duplication system. They’re respectively written as [-x] body , (f -x) and {-x : A} B .

3f. Annotations