Big Steps, Small Steps
Learning Objectives
- You understand big-step call-by-name evaluation semantics for lambda calculus.
- You can derive evaluation trees using the call-by-name rules.
- You understand single-step Ξ²-reduction and weak-head normal forms.
- You can implement call-by-name evaluation and Ξ²-reduction step functions in code.
Introduction
Now that we understand and have implemented capture-avoiding substitution, we can define and implement the semantics of lambda calculus. As there are only three kinds of terms, the semantics are really simple on the surface. In fact, most of the complexity lies in the capture-avoiding substitution.
In Variables and Abstraction we briefly mentioned evaluation in lambda calculus. We begin by defining big-step operational semantics and then move to defining small-step semantics for lambda calculus.
From now on, we use the convention that lambda terms are equal, if they are equal up to -equivalence. This means that when we are working with the term it is the same as β we say they are mathematically equal.
Big-Step Semantics for Lambda Calculus
Unlike the languages studied in the previous chapter, lambda terms donβt evaluate to an integer or a Value.
Instead, lambda terms evaluate to lambda terms, and being a value, almost self-referentially, means being the outcome of evaluation.
To actually make any sense of this idea, letβs look at some examples next. We denote big-step evaluation by the usual symbol. The statements have a βRust analogβ using closures next to them.
Now, letβs define the semantics of lambda calculus. As there are a lot of different ways to define semantics for the lambda calculus, called reduction strategies1 we opt for one of the simplest: call-by-name or CBN.
As we saw in the example, variables donβt evaluate, or rather they evaluate to themselves β variables only play a role in substitution. In fact, if the term is βwell-typedβ we should never need to evaluate a free variable. This is because values are always closed when evaluating well-typed terms.
What it means for a term to be well-typed is defined in the next chapter on the simply typed lambda calculus. For now you can think of well-typed terms primarily as closed terms.
As abstractions house an inner term, we could have a rule to evaluate inside the abstraction. However, this is unnecessary, and done in normalization strategies (which we study in the next chapter) as opposed to the call-by-name strategy.
We will have the following two base rules for the call-by-name evaluation.
They state that any variable or abstraction evaluates to itself.
Another evaluation rule in CBN is one that involves both an abstraction and an application. This is where the βmagicβ happens. If evaluates to an abstraction, we have an abstraction applied to a term β so we can perform a substitution!
Take a moment to read what the rule actually says. Are all the assumptions necessary? Itβs important that the substitution is capture-avoiding β why?
Finally, we add an evaluation rule to use the result of evaluating in the previous rule. This rule can only be used if doesnβt evaluate to an abstraction (or rather evaluates to a nonabstraction), which we denote with .
As an example, letβs write down the evaluation tree of an identity function applied to the identity function
As is -equivalent with , it doesnβt matter what the actual bound variables are. Of course a computational implementation has to decide which bound variables to use (or which fresh variables to use in capture-avoiding substitution), but thatβs just an implementation detail which has no effect on the theory.
Here are some more examples of call-by-name evaluation:
Notice that in the examples, evaluating terms that contain free variables may result in a term which is a variable or even an application. The set of terms that result from CBN evaluation are said to be in weak-head normal form.
Implementing Call-By-Name Evaluation
As the call-by-name evaluation is just a relation, we would like to turn it into at least a partial function.
The function, called cbn evaluates a term by computing the value of ? that evaluates to:
This makes us ask a couple of important questions about the relation:
- Does
?always exist? If so, is left-total. - Is the value of
?unique for a given ? If so, is functional. - Can we write an algorithm to compute
??
We put questions 1 and 2 aside for now and instead focus on question 3 by implementing an algorithm that does call-by-name evaluation.
Big-Step Call-By-Name Evaluation
0 / 5 points
Which of the following statements about big-step call-by-name evaluation are true?
Call-By-Name Evaluation (Big-Step)
0 / 30 points
Implement cbn according to the call-by-name semantics described in the material.
Examples
For example, the following terms should evaluate to the results on the right (up to alpha-equivalence).
(x => y => x) (y => z => y) β y => y => z => y (x => x) ((x => x) y) β y (x => y => x) (z y) β y_ => z y (x => y => x) (z => y) β y_ => z => y (x => y x) (z => z) β y (z => z) (x => x y) (z => z) β yGrading
Only the cbn function is tested.
The term it returns is compared to the expected term up to alpha-equivalence, i.e. you can use any fresh variables you want as long as the result is alpha-equivalent with the correct term.
t! Macro
A macro t! is provided to make it much more convenient to write lambda terms in Rust.
For example t!(x => y => x) expands to the lambda term Ξ»x. Ξ»y. x.
The macro also supports embedding an external lambda term using an exclamation mark.
For example, if t is the lambda term Ξ»y. x then t!(x => !t) expands to the lambda term Ξ»x. Ξ»y. x.
Single-Step Semantics
Now that we have a basic understanding of evaluation of lambda terms, we expand it with the notion of small-step semantics by studying the single-step -reduction. Instead of evaluating the whole term at once, -reduction happens step-by-step. -reduction also tends to be nonfunctional meaning that a term can take different reduction steps, e.g. reducing the function or the argument position in an application β or any reducible subterm. We denote (single-step) -reduction using an arrow symbol .
-reduction has many slightly different formulations1 and has a lot of literature written about it. We highly recommend reading chapters 2.4 and 2.5 from Selinger: Lecture Notes on the Lambda Calculus.
Weak -Reduction
The notion of -reduction is much more general than defined here. We will learn another, stronger version of it in the next chapter.
Our first version will be like the call-by-name evaluation studied earlier as it doesnβt evaluate the arguments at all. It gets the name weak, because it doesnβt reduce under abstractions and produces weak-head normal forms when reducing until no reductions can be made.
The first, and most important, rule called AppAbs is the βheart of the lambda calculusβ.
Letβs go over it thoroughly.
The rule states that an application term where the left-hand-side is an abstraction reduces to the substitution of by in which is the term .
Here it is written as an inference base rule without premises:
Notice that the AppAbs rule allows us to only perform one substitution at a time, so e.g.
but is not a -reduction step directly.
Next, we explore the other single-step -reduction rules. These rules are like βadaptersβ that let us reduce subterms. For -reduction to do anything useful, we need a way to reduce the left subterm of an application:
This is because the only other rule AppAbs requires an abstraction as the left subterm of an application.
Reducing the right subterm does not get us any closer to being able to use AppAbs.
One can think of
AppAbsas the rule that drives progress. Every other rule is there to get us closer to usingAppAbsor to simplify the lambda term to a nicer form.
Valid AppAbs Instances
0 / 5 points
Which of the following are instances of the AppAbs rule?
Valid App1 Instances
0 / 5 points
Which of the following are conclusions of an instance of the App1 rule whose premises are valid?
Weak Single-Step Beta-Reduction (Small-Step)
0 / 45 points
- First, fill in your previous implementation.
- Next, implement
app_abs,app1such thatapp_absattempts to apply theAppAbsrule,app1attempts to apply theApp1rule. - Finally, implement
stepwhich attempts to reduce using eitherapp_absorapp1.
If any application of a rule fails, return None.
Note: app1 should use step to reduce the inner term.
Hint: use Option::or_else to combine the results from self.app_abs() and self.app1().
Examples
(x => y => x) (y => z => y) --> y => y => z => y (x => x) ((x => x) y) --> (x => x) y (x => y => x) (z y) --> y_ => z y (x => y => x) (z => y) --> y_ => z => y (x => y x) (z => z) --> y (z => z) (x => x y) (z => z) --> (z => z) y x (z => z) --> NoneGrading
Only the app_abs, app1, and step functions are tested.
The terms these functions return are compared to the expected terms up to alpha-equivalence.
Now that you hopefully found a working implementation, the following quizzes will probe your understanding a bit further.
A key detail to unlock the correct answers is to think about the relationship between the AppAbs and App1 rules.
For example, is it possible for to be an abstraction in the premise of App1?
Order of Rules in Implementation
0 / 5 points
What happens if the order in which App1 and AppAbs are attempted is swapped in the implementation of step?
Weak Beta-Reduction
0 / 8 points
Which of the following statements are true?
Footnotes
Footnotes
-
See for example Sestoft: Demonstrating Lambda Calculus Reduction or Wikipedia: reduction strategy. β© β©2