-
Notifications
You must be signed in to change notification settings - Fork 15
Types and Programming Languages Chapter 4 An ML Implementation of Arithmetic Expressions
## Our implementations
Before the meeting, a large number of people implemented code based on the material in chapters 3 and 4:
- @beng’s Swift implementation
- @urbanautomaton’s Prolog and Ruby implementations
- @mudge’s Rust implementation
- @leocassarani’s Haskell implementation
- @dkandalov’s Kotlin implementation
- @richardcooper’s Python implementation
- @tuzz’s Ruby implementation
- @tuzz’s Turing Machine implementation
- @lsrus’s Racket implementation
- @zetter’s Elixir implementation
- @SimonWoolf’s Elixir implementation
- @tomstuart’s Ruby implementation
- @jcinnamond’s Parslet implementation
- @h-lame’s Ruby implementation
- @eightbitraptor’s Scheme implementation
We started by discussing what we had nominally planned for the meeting. We were going to mob program an implementation of the eval
function from chapter 4. We agreed we should reserve plenty of time (a full hour) for show and tell as lots of people had built things.
We considered which language to use. We settled on JavaScript as it was one of the few languages no one had chosen beforehand and the majority of people were loosely familiar with it.
We decided we'd like to implement eval
by figuring things out for ourselves, rather than basing it on the implementation in the book. We started by considering the boolean rules. Tom wrote them up on the whiteboard:
t ::= true
false
if t then t else t
if true then t₂ else t₃ → t₂
if false then t₂ else t₃ → t₃
t₁ → t₁′
----------------------------------------------
if t₁ then t₂ else t₃ → if t₁′ then t₂ else t₃
We collectively came up with some test cases relating to these rules, e.g. if true then true else false
. We'd use these test cases to drive out a solution in a TDD-esque manner. Paul offered to type.
We decided to use CodePen and opted for some low-fi console.log
testing.
How should we represent terms? Functions? Objects? Arrays?
We agreed to use constructor functions for a number of reasons, including:
- it would be easier to add methods to our objects if we decided to
- it feels quite natural to think in terms of objects in our programs (Murray)
- it would be easier to reference their properties, e.g.
t.t3
rather thant[2]
We focussed on the eval1
function which performs a small-step evaluation of a term. We spoke about how to handle the "no rule applies" case. We agreed to come back to this once we had developed our code a bit further.
We then moved on to eval
which needed to call eval1
multiple times. We had two options:
We can either use an explicit looping construct such as a while loop or we could use recursion.
Recursion seemed to be a popular choice. Paul sketched out the code and Leo explained how it worked.
We added more rules to implement, this time including succ
, pred
and iszero
. We still needed to deal with the "no rule applies" problem but decided to keep going. We didn't really have enough implemented to be able to construct good test cases for this yet.
There was a brief interlude. It was generally agreed that adding salt to coffee was a bad idea. Someone pointed out that people seem to add almost anything to coffee, e.g. butter and rum. @tuzz proposed banoffee coffee and toffee coffee. Everyone rolled their eyes applauded profusely.
We briefly talked about whether we should return a new Zero
object for E-PredZero
or return the existing Zero
object from t1
. This was quickly cleared up by following the rule literally and returning a new Zero
object.
We proceeded to fill in the remaining rules. We were approaching an hour into the meeting so we made the decision to park the implementation in its current state. Things that were left:
- adding guard conditions around nested
eval1
calls for things where "no rule applies" - implementing
isNumericVal
and adding it as a condition to the appropriate rules e.g.(pred (succ true))
A cleaned up version of our code is available on GitHub.
(The implementations are listed at the top of this page.)
Ben
Ben used Swift to implement the eval
function. He used indirect enum
in a few places. He noted it was more-or-less a literal translation of the code in the book. He used pattern matching in his case statement. There was a brief discussion about the use of where
in the case statement. Ben explained that where
is part of Swift's pattern matching and can be used outside of case
as well. He wasn't sure whether it was idiomatic Swift. He suggested that option types might be more idiomatic.
Simon
Simon used Prolog. There was some discussion about premises being written after the rule. This differs from the style in the book. His implementation referred to eval1
as reduce
which some felt was a better name for this function. Simon pointed out that Prolog lends itself well to this kind of problem. So well, in fact, that we're able to run the program backwards to generate terms:
Leo pointed out this this "drives the point home" that we're dealing with an infinite set. We can continually apply the rules to build bigger terms.
Paul
Paul's implementation was in Rust. He hoped, before he started, that Rust would be a good language for this as it supports pattern matching but then 'immediately ran into problems'. Specifically, problems relating to the ownership of objects. We noted that indirect
from Ben's Swift implementation was similar to the use of box
in Paul's. This is a way to tell the static compiler to use the heap rather than the stack for storing terms. By default, Rust prefers the stack. Paul made use of the option
monad to handle eval1
calls on terms that couldn't be evaluated any further.
Dmitry
Dmitry used Kotlin which he explained is a relatively new language that's related to Scala and Java. He didn't go into too much detail, but explained he had implemented the eval
function as well.
Leo
Leo used Haskell and pointed out it was a straightforward implementation (no need for 'indirect' or 'box'). He implemented the eval
function which used the small-step semantics. He then explained he'd experimented with QuickCheck, which is a property testing framework. He took the theorems from chapter 3 and generated random terms which he fed through these property tests.
In cases where a violation of some property was found, Leo implemented a custom shrinking function which attempts to find the simplest possible counter-example that demonstrates the problem. He pointed out that you don't have to write your own shrinking function. He'd had a go at implementing the E-Funny
rules from chapter 3 to demonstrate that they violated some of the theorems. He also implemented eval
using big-step semantics then used QuickCheck to demonstrate that the two implementations were equivalent.
Rich
Rich's implementation was in Python. He explained it was similar to the other implementations. He explicitly threw exceptions and used an explicit while loop rather than recursion for the eval
function. He implemented a constructor function to build Term classes and explained it was a bit unusual to define classes inside a function. He then refactored his code to use functions for each rule rather than the case statement.
Rich implemented the 'funny' rules as well and showed they violated the theorems. He then spoke about how he used yield
to replace the explicit error handling. In cases where no further evaluation could take place, nothing would be yielded (an analogy was made to an empty list). He then moved on to attempting to write a pattern matching framework in Python.
Finally, Rich explained that he'd used Jupyter which is a tool that is commonly used when scientists work with large data sets. It allows you to evaluate expressions in real time and display their derivation trees. He'd used nested html tables for this. The isNumerivVal
condition became just another rule in these derivation trees.
Laura
Laura used Racket. She was hesitant at first to demo her implementation, but Leo was keen to see some Racket code. She explained she'd used Racket's pattern matching features as well as guard clauses.
Chris P
Chris had implemented eval
in Ruby and took the book's implementation quite literally. He explained he'd have liked to implement a polymorphic solution which moved behaviour onto the term subclasses. He briefly showed a graph that represents his Turing machine implementation.
Chris had implemented the rules for a Turing machine that evaluated terms. He explained the online environment he'd used only supported single-letter symbols which made it quite difficult to make sense of. He showed the Turing machine running to reduce a non-trivial expression to false
.
He explained that his Turing machine operated in a right-left direction of the input. Much of the implementation was taken up with the monotony of copying symbols around on the tape. He demonstrated an example that used lots of pred
and succ
and showed this copying process.
Chris Z
Chris used Elixir and had made use of its pattern matching features and guard clauses. He'd tried to change eval
to work without throwing exceptions.
Simon
Simon also used Elixir with a very simple lexer and parser to parse a string input into nested tuples, and a small step and big step evaluator. Each used a different workaround to the problem in Elixir of not being able to have arbitrary recursive functions in guard clauses for is_numeric etc. (the small-step capped the maximum recursion depth; the big-step used a custom conditional macro rather than the built-in case clause).
Tom
Tom had implemented a parser and evaluator for the metalanguage used in the book for defining grammars. He'd then used this metalanguage to define the specific grammar defined in chapters 3 and 4. Like Rich, he needed to define a syntactic category for nv
.
We discussed the possibility of using Tom's metalanguage in future meetings for other grammars the book defines. He suggested it might just work without additional changes.
John
John had implemented eval
in Ruby through the use of parslet. He explained that he'd exploited parslet's support for transforming trees in order to actually perform the evaluation of expressions.
At the end of the meeting, we held a very brief retrospective as we'd already overrun. We agreed we should:
- timebox show and tells so that everyone has an equal amount of time to present
- be more prepared for mobbing, e.g. set up a simple test harness ahead of time
We questioned whether mob programming will work with more complicated chapters. John noted that we'll likely be ok for a little while longer as the next chapters are about the lambda calculus, which we collectively have some knowledge of.
Tom explained that he felt encouraged by this meeting. It's a challenging book and we seem to have collectively got a grip on the main concepts.
Finally, we talked about when to hold the next meeting as some people are away. Deferred to Slack.
## Pub
A few of us went to the pub.
- 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