-
Notifications
You must be signed in to change notification settings - Fork 15
Types and Programming Languages Chapter 14 Exceptions
Before the meeting, @tomstuart sent the following update to the club mailing list:
Hi Computation Club,
We’re meeting tomorrow night (Tuesday 8th) to discuss chapter 14 of Types and Programming Languages. Please sign up if you’re planning to join us, so we don’t accidentally bring too few or (god forbid) too many snacks.
[…]
It’s been a quiet couple of weeks since our last meeting. Some of this is doubtless due to the soporific effect of this dreamy summer that feels like it’ll never end, smothering our minds with its heat and beauty like warm maple syrup poured over a sentient waffle, transporting us to a half-remembered childhood of lemonade, wildflowers, jumpers for goalposts, bark rubbings, fake bokeh, those funny feet ice lollies you remember the ones, etc. But another factor might be that chapter 14 is quite short, relatively straightforward and about exceptions.
Previous chapters have introduced new and exciting types in order to accommodate new and exciting features in the operational semantics; chapter 14 gives us some new syntax and some new typing rules, but doesn’t affect the syntax of types at all. As with mutable references in chapter 13, we may end up spending the majority of the meeting discussing how the syntax and operational semantics give rise to exception-like behaviour in the lambda calculus, and only briefly touch on the typing rules, which are less complicated and thus a little less educational than last time. Still, it’s an interesting demonstration of how we can add a recognisable language feature while maintaining the safety (= progress + preservation) property that we care about so much.
I like this chapter because it continues to illustrate how much of programming language design is about decisions, tradeoffs and judgement. It’s showing us that we can engineer the evaluation behaviour we want without sacrificing runtime properties we care about (i.e. determinism), but that as a consequence we have to abandon some other important properties (i.e. uniqueness of types) to accommodate this in the type system, and therefore have to do some patching up elsewhere (i.e. redefine progress) to make everything hold together. This interplay between the different parts of the language is incredibly common and strongly informs the way that language designers choose and combine features in the presence of some kind of static type system. It’s cool that we’re getting a feel for that engineering process with such a simple language and in such a superficially mathematical context.
Also! This is the last chapter in part II of the book! What are we going to do after this? Do we barrel straight into 130 pages of subtyping, or take a break and do something else, or call time on the whole adventure? I will TRY to RAISE this tomorrow.
Looking forward to seeing you there.
Cheers,
-Tom
We began the meeting with the traditional decanting of babas ganoush and hummus and @tuzz boldly took to slicing up various breads including one containing cranberries brought by @jcoglan which was much remarked upon.
Suitably loaded with carbohydrates, we kicked off discussion of the chapter.
We began by discussing the "big picture" of the chapter: the introduction of a new feature in our language to allow users to signal errors at any point (and later handle them). We (and the chapter) discussed how this is a design choice: we could not introduce errors with new syntax but instead rely on variant/sum types as introduced in Types and Programming Languages Chapter 11 Simple Extensions.
We briefly highlighted examples of "real" programming languages that make similar design choices, e.g. Ruby's raise
:
raise "Some error"
Or JavaScript's throw
:
throw 'Something went awry!';
As opposed to Rust's explicit error handling through a Result
type:
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
Satisfied that we were familiar with the general concept of exceptions, we marched on to introduce some new syntax:
t ::= …
error
This introduces a new term in our simply-typed Lambda Calculus, error
, which is used to signal an error/exception in our program.
The evaluation rules for this new term were as follows:
error t2 ⟶ error (E-AppErr1)
v1 error ⟶ error (E-AppErr2)
This means a few things:
- Trying to evaluate an application where the left-hand side (typically where you'd find an abstraction) is
error
evaluates toerror
, discarding the right-hand term; - Trying to evaluate an application where the left-hand side is a value (e.g. an abstraction) and the right-hand side is
error
also evaluates toerror
, discarding the left-hand term.
One subtle thing to note is that error
is not a value otherwise it would introduce ambiguity into our evaluation rules.
We decided to work through some examples on the board to firm up our understanding of these rules:
@tomstuart wrote up the following example on the whiteboard and we stepped through its evaluation:
let decrement = λn:Nat . if iszero n then error else pred n
in iszero (decrement 0)
(Note that this chapter builds on the simply-typed Lambda Calculus, not the simple extensions but we felt it was OK to cheat a bit here to make a more interesting example.)
First, we desugared the let
:
iszero ((λn:Nat . if iszero n then error else pred n) 0)
Then we began to evaluate the term being applied by evaluating its application:
iszero (if iszero 0 then error else pred 0)
Then we evaluated the condition of the if
:
iszero (if true then error else pred 0)
Then, as the condition was true
, we could evaluate the if
by replacing it with its consequent:
iszero error
And this allowed us to use our new evaluation rule E-AppErr2
(assuming we can treat iszero
as a value as it is effectively an abstraction):
error
So far, so straightforward. @mudge tried to jazz this up a bit:
((λx.Nat→Nat . x) (λy:Nat . 0)) error
We first evaluate the left-hand side (as it is not yet a term):
(λy:Nat . 0) error
Then we can apply E-AppErr2
as the left-hand side is now a value (as it is an abstraction):
error
@jcoglan then stepped up to give an example that exercised our other evaluation rule, E-AppErr1
:
((λy:Nat . 0) error) ((λy:Nat . y) 0)
Again, we evaluate the left-hand side and find that it matches E-AppErr2
:
error ((λy:Nat . y) 0)
This now matches E-AppErr1
as, even though the right-hand side is not yet a value, our evaluation rule tells us to abandon it:
error
Confident in our ability to evaluate errors, we then moved on discussing its rather plain-looking typing rule:
-------------
Γ ⊢ error : T
In order to remain well-typed, we learnt how error
can have any type, a concept we haven't yet seen in the book.
For example, type checking the following application:
(λy:Nat . y) error
Would result in error
having type Nat
.
We discussed alternatives to having error
be of any type (the chapter explaining that we could achieve this with subtyping or parametric polymorphism neither of which we have covered yet), with @leocassarani proposing that we could have evaluation use information from the type checker to populate an ascription automatically:
The chapter provides an example of a well-typed term using ascription on error
that would break the preservation property of our type system:
(λx:Nat . x) ((λy:Nat . 5) (error as Bool))
As this would evaluate in one step to an ill-typed term:
(λx:Nat . x) (error as Bool)
Interestingly, the introduction of this new, non-value error
term means that we have to refine our definition of progress:
Suppose
t
is a closed, well-typed normal form. Then eithert
is a value ort = error
.
We discussed how this wording was unlike our previous definition (with meaning packed into the use of the phrase "normal form") but could be translated like so:
If a closed term
t
is well-typed, then either it can be evaluated, is a value or iserror
.
Suitably well-typed, we marched onto the next new feature of our language: exception handlers.
We introduced another brand new bit of syntax:
t ::= …
try t with t
This is reminiscent of the familiar try
catch
from languages such as JavaScript and PHP:
try {
// ...
} catch {
echo 'Jings crivens, help me boab!';
}
With it came some new evaluation rules:
try v1 with t2 ⟶ v1 (E-TryV)
This first rule shows that if we evaluate a term until the "body" of the try
is a value, the next step of evaluation is to use that same value and discard the exception handling part in t2
.
try error with t2 ⟶ t2
On the flip side, if the body of the try
evaluates to an error
, we should return the handler t2
.
Finally, we need a congruence rule:
t1 ⟶ t1'
----------------------------------
try t1 with t2 ⟶ try t1' with t2
This shows that if the body of the try
can be evaluated (t1 ⟶ t1'
) then we do that a single step.
This intuitively matches how you'd expect exception handlers to work: evaluate the body and, if it raises an exception, fire the handler, otherwise return the successful result of the body.
We also have a new (but not unfamiliar) typing rule for try
:
Γ ⊢ t1 : T Γ ⊢ t2 : T
-----------------------
Γ ⊢ try t1 with t2 : T
This shows that try t1 with t2
has a type T
if both the body t1
and the handler t2
return the same type T
(not dissimilar to how we type-check if
).
Finally, we raced through the last new feature: the ability to raise an exception with some value using raise
.
t ::= …
raise t
try t with t
This replaces error
with raise t
which allows us to pass some term t
when we signal an exception.
The evaluation rules match those we saw for error
:
(raise v11) t2 ⟶ raise v11 (E-AppRaise1)
v1 (raise v21) ⟶ raise v21 (E-AppRaise2)
With an added congruence rule for evaluating the term of a raise
:
t1 ⟶ t1'
----------------------
raise t1 ⟶ raise t1'
And a rule to handle exceptions raised when evaluating the term passed to another exception:
raise (raise v11) ⟶ raise v11
We also restate the rules of try
to accommodate raise
:
try v1 with t2 ⟶ v1
t1 ⟶ t1'
----------------------------------
try t1 with t2 ⟶ try t1' with t2
We also explain what happens to the term passed to raise
:
try raise v11 with t2 ⟶ t2 v11
Namely, that the handler t2
is called with it! This means that the handler t2
should be some sort of function which leads us neatly onto the typing rules:
Γ ⊢ t1 : Texn
-----------------
Γ ⊢ raise t1 : T
This is similar to the error
rule we saw previously in that raise t1
has any type T
but it also states that the term passed to raise
should be some predefined type Texn
. This may well be a Nat
(e.g. if you want to use numeric error codes) or perhaps some Exception
type but again this all seemed like a tease for subtyping.
Γ ⊢ t1 : T Γ ⊢ t2 : Texn → T
------------------------------
Γ ⊢ try t1 with t2 : T
Finally, we refined our typing rule for try
such that the handler t2
must now be a function of the exception type Texn
to the same T
as the body t1
.
With that, we were done with exceptions!
As we finished a little early, we had chance to discuss some of the topics raised by @tomstuart in his update about how this chapter demonstrates a programming language design decision and its knock-on effect on our type system.
We also remarked on the surprising existence of this very specific sign in the Geckoboard office:
As we had more time than usual, we took the time to reflect on both the meeting and TAPL in general:
- We were all pleased to have made it this far into the book particularly as it feels like the end of an important first chunk of the material;
- We were eager to continue to at least cover subtyping both after so much teasing in this chapter and because the dependency graph at the beginning of the book shows it to be critical to so many other topics;
- We decided to not pause for an interstitial (as we had one recently) but for the next meeting to be about Chapter 15;
- @mudge volunteered to organise the next meeting;
- We discussed whether we'd need to split Chapter 15 into more than one meeting as it seems rather lengthy and dense and decided that a few people would research a sensible plan to be announced before the next meeting;
- We discussed whether, from this point on, we should pay more attention to the aforementioned dependency graph in the book for choosing "interesting" chapters and then bailing on the book once we reach a consensus;
- We acknowledged the ever-present issue of the club being difficult to join mid-book and decided this continued pressure should be part of our decision every book meeting whether to press on or switch to a new book.
- @mudge discussed recent struggles with reliable queueing and testing randomness which led to discussion of the Birthday paradox and much pondering about the distribution of birthdays
-
@mudge discussed his recent adventures solving distributed systems problems with Redis, specifically using
SET NX EX
for distributed locking, Redlock and Martin Kleppmann's criticism of it - @leocassarani discussed similar adventures with Consul for interrupting work and leader elections
- We discussed the scarcity of Hammock-Driven Development and whether that was a good or bad thing (given, you know, capitalism)
Thanks to @leocassarani and Geckoboard for hosting and providing beverages (both soft and not), @tuzz for organising the meeting, all those that brought bread, snacks and dips and to @tomstuart for leading the meeting.
- Home
- Documentation
- Choosing a Topic
- Shows & Tells
- Miscellaneous
- Opt Art
- Reinforcement Learning: An Introduction
- 10 Technical Papers Every Programmer Should Read (At Least Twice)
- 7 More Languages in 7 Weeks
- Lua, Day 1: The Call to Adventure
- Lua, Day 2: Tables All the Way Down
- Lua, Day 3
- Factor, Day 1: Stack On, Stack Off
- Factor, Day 2: Painting the Fence
- Factor, Day 3: Balancing on a Boat
- Elm, Day 1: Handling the Basics
- Elm, Day 2: The Elm Architecture
- Elm, Day 3: The Elm Architecture
- Elixir, Day 1: Laying a Great Foundation
- Elixir, Day 2: Controlling Mutations
- Elixir, Day 3: Spawning and Respawning
- Julia, Day 1: Resistance Is Futile
- Julia, Day 2: Getting Assimilated
- Julia, Day 3: Become One With Julia
- Minikanren, Days 1-3
- Minikanren, Einstein's Puzzle
- Idris Days 1-2
- Types and Programming Languages
- Chapter 1: Introduction
- Chapter 2: Mathematical Preliminaries
- Chapter 3: Untyped Arithmetic Expressions
- Chapter 4: An ML Implementation of Arithmetic Expressions
- Chapter 5: The Untyped Lambda-Calculus
- Chapters 6 & 7: De Bruijn Indices and an ML Implementation of the Lambda-Calculus
- Chapter 8: Typed Arithmetic Expressions
- Chapter 9: The Simply-Typed Lambda Calculus
- Chapter 10: An ML Implementation of Simple Types
- Chapter 11: Simple Extensions
- Chapter 11 Redux: Simple Extensions
- Chapter 13: References
- Chapter 14: Exceptions
- Chapter 15: Subtyping – Part 1
- Chapter 15: Subtyping – Part 2
- Chapter 16: The Metatheory of Subtyping
- Chapter 16: Implementation
- Chapter 18: Case Study: Imperative Objects
- Chapter 19: Case Study: Featherweight Java
- The New Turing Omnibus
- Errata
- Chapter 11: Search Trees
- Chapter 8: Random Numbers
- Chapter 35: Sequential Sorting
- Chapter 58: Predicate Calculus
- Chapter 27: Perceptrons
- Chapter 9: Mathematical Research
- Chapter 16: Genetic Algorithms
- Chapter 37: Public Key Cryptography
- Chapter 6: Game Trees
- Chapter 5: Gödel's Theorem
- Chapter 34: Satisfiability (also featuring: Sentient)
- Chapter 44: Cellular Automata
- Chapter 47: Storing Images
- Chapter 12: Error-Correcting Codes
- Chapter 32: The Fast Fourier Transform
- Chapter 36: Neural Networks That Learn
- Chapter 41: NP-Completeness
- Chapter 55: Iteration and Recursion
- Chapter 19: Computer Vision
- Chapter 61: Searching Strings
- Chapter 66: Church's Thesis
- Chapter 52: Text Compression
- Chapter 22: Minimum spanning tree
- Chapter 64: Logic Programming
- Chapter 60: Computer Viruses
- Show & Tell
- Elements of Computing Systems
- Archived pages