Continuing where I left off: we were talking about arity in Martin-Löf’s theory of expressions. There are two basic problems that arity solves: it makes certain kinds of impossible-to-evaluate expressions be invalid in the theory; and it helps enable some way of comparing expressions for equality. Arity solves both of those problems by imposing a simple type system over expressions.
At the end of the last post, I started giving a sketch of what arities look like. Now we’re going to dive in, and take a look at how to determine the arity of an expression. It’s a fairly simple system of rules.
Before diving in, I want to stress the most important thing about the way that these rules work is that the expressions are totally syntactic and static. This is something that confused me the first time I tried to read about expression theory. When you see an expression, you think about how it’s evaluated. But expression theory is a purely syntactic theory: it’s about analyzing expressions as syntactic entities. There are, deliberately, no evaluation rules. It’s about understanding what the notations mean, and how to determine when two expressions are equivalent.
If, under the rules of Martin-Löf’s expression theory, two expressions are equivalent, then if you were to chose a valid set of evaluation rules, the two expressions will evaluate to the same value. But expression equivalence is stronger: expressions are equivalent only if you can prove their equivalence from their syntax.
That clarified, let’s start by looking at the rules of arity in expressions.
- Variables and Constants
- Every variable and every primitive constant has a pre-defined arity; if is a variable or primitive constant with arity , then the expression has arity .
- In a definition , the arity of the defined name is the same as the arity of the expression .
- If is an expression of arity , and is a expression of arity , then is an expression of arity .
- If is an expression of arity and is a variable of arity , then is an expression of arity .
- If is an expression af arity , is an expression of arity , …, and is an expression of arity , then a combination expression is an expression of arity .
- If is an expression of arity where , then is an expression af arity .
Let’s try working through an example: .
- As we saw in this post, this is equivalent to the simple AST-form: .
- “” is a variable of arity 0; “3” and “7” are constants of arity 0; “” and “” are constants of arity .
- From the combination rule, since and both have arity 0, and each have arity .
- Since has arity , and has arity , has arity 0. The same thing works for .
- Since the arities of the and are both 0, the combination of the pair (the arguments to the inner “+”) are , and the arity of the inner sum expression is thus 0.
- Since 7 has arity 0, the combination of it with the inner sum is , and the arity of the outer sum is 0.
- Since is a variable of arity 0, and outer sum expression has arity 0, the abstract has arity .
If you’re familiar with type inference in the simply typed lambda calculus, this should look pretty familiar; the only real difference is that the only thing that arity really tracks is applicability and parameter counting.
Just from this much, we can see how this prevents problems. If you try to compute the arity of (that is, the selection of the first element from 3), you find that you can’t: there is no arity rule that would allow you to do that. The selection rule only works on a product-arity, and 3 has arity 0.
The other reason we wanted arity was to allow us to compare expressions. Intuitively, it should be obvious that the expression and the expression are in some sense equal. But we need some way of being able to actually precisely define that equality.
The kind of equality that we’re trying to get at here is called definitional equality. We’re not trying to define equality where expressions and evaluate to equal values – that would be easy. Instead, we’re trying to get at something more subtle: we want to capture the idea that the expressions are different ways of writing the same thing.
We need arity for this, for a simple reason. Let’s go back to that first example expression: . Is that equivalent to ? Or to ? If we apply them to the value 3, and then evaluate them using standard arithmetic, then all three expressions evaluate to 25. So are they all equivalent? We want to be able to say that the first two are equivalent expressions, but the last one isn’t. And we’d really like to be able to say that structurally – that is, instead of saying something evaluation-based like “forall values of x, eval(f(x)) == eval(g(x)), therefore f == g”, we want to be able to do something that says because they have the same structure.
Using arity, we can work out a structural definition of equivalence for expressions.
In everything below, we’l write to mean that has arity , and to mean that and are equivalent expressions of arity . We’ll define equivalence in a classic inductive form by structure:
- Variables and Constants
- If is a variable or constant of arity , then . This is the simplest identity rule: variables and constants are equivalent to themselves.
- If is a definition, and , then . This is a slightly more complex form of an identity rule: if there’s a definition of as the value of , then and are equivalent.
- Application Rules
- If and , then . If an applyable expression is equivalent to another applyable expression , then applying to an expression is equivalent to applying to an expression if the argument is equivalent to the argument . That’s a mouthful, but it’s simple: if you have two function application expressions, they’re equivalent if both the function expressions and the argument expressions are equivalent.
- If is a variable of arity , and is an expression of arity and is an expression of arity , then . This is arity’s version of the classic beta rule of lambda calculus: applying an abstraction to an argument means substituting the argument for all references to the abstracted parameter in the body of the abstraction.
- Abstraction Rules
- If is a variable of arity , and , then . If two expressions are equivalent, then two abstractions using the same variable over the same expression is equivalent.
- If and are both variables of arity , and is an expression of arity , then , provided is not free in .
Basically, the renaming variables in abstractions don’t matter, as long as you aren’t using the variable in the body of the abstraction. So is equivalent to , but it’s not equivalent to , because is a free variable in , and the abstraction would create a binding for .
This is arities version of the eta-rule from lambda calculus: If is a variable of arity , and is an expression of arity , then . This is a fancy version of an identity rule: abstraction and application cancel.
- Combination Rules
- If , , …, , then . This one is simple: if you have two combination expressions with the same arity, then they’re equivalent if their elements are equivalent.
- If , then . Another easy one: if you take a combination expression, and you decompose it using selections, and then recombine those selection expressions into a combination, it’s equivalent to the original expression.
- Selection Rules
- If , then . This is the reverse of combinations rule one: if you have two equivalent tuples, then their elements are equivalent.
- If , then . An element of a combination is equivalent to itself outside of the combination.
- If , then .
- If , then .
- If , and , then .
Jumping back to our example: Is equivalent to ? If we convert them both into their canonical AST forms, then yes. They’re identical, except for one thing: the variable name in their abstraction. By abstraction rule 2, then, they’re equivalent.