The Art of Interpretation – Part 2
In software development, crafting well-designed code is an art form. This blog series is part one of key takeaways from the book “Software Design by Example” by Greg Wilson, focusing on the key principles to learn software designing as building blocks.
In software development, crafting well-designed code is an art form. This blog series will explore my key takeaways from the book Software Design by Example by Greg Wilson, focusing on a unique approach to mastering design principles. The book assumes a basic understanding of Python and Unix shell commands.
Two Ways to Run Code
Compilers and interpreters are just programs. They both serve the purpose of translating a high-level programming language into machine code that a computer’s processor can execute, but they do so in different ways:
Compilers vs. Interpreters
🔹 Translation Time
- Compiler: Translates the entire program into machine code before any code runs.
- Interpreter: Translates the program into machine code on the fly, line by line, as it runs.
🔹 Execution
- Compiler: Once compiled, the program can be executed multiple times without recompiling. Machine code runs directly on the hardware.
- Interpreter: Interprets and runs the program line by line every time it runs.
🔹 Performance
- Compiler: Generally faster since the translation happens only once.
- Interpreter: Slower as it translates and executes concurrently.
🔹 Examples
- Compiler: C, C++, Rust
- Interpreter: Python, Ruby, JavaScript
Blurry Lines in Practice
In reality, the distinction between compilers and interpreters is not always clear-cut. Many languages and their implementations use a mix of both approaches. For instance, Python:
- Bytecode Compilation: When a Python program runs, the Python interpreter first compiles the source code into an intermediate form known as bytecode. This bytecode is not machine code but a lower-level, platform-independent representation of the source code.
- Caching Bytecode: To improve performance, Python saves the compiled bytecode in .pyc files. The next time the program runs, Python can skip the compilation step if the bytecode is up-to-date, directly interpreting the bytecode instead of the source code.
- Interpreting Bytecode: The Python virtual machine (PVM) then interprets the bytecode, which translates it into machine code just in time for execution. This approach gives Python some of the benefits of both compilation (like saving time on subsequent runs) and interpretation (like flexibility and ease of debugging).
Got it! Here’s the complete content in clean Markdown format without fenced code blocks breaking the flow, so you can just copy-paste it as a full .md
file:
Building a Simple Expression Interpreter
We’ll build an interpreter that can handle operations like addition using nested lists to represent the expressions by using prefix notation where the operation comes first, followed by its operands. For example:
["add", ["abs", -5], 9]
representsabs(-5) + 9
["add", 1, 2]
represents1 + 2
["abs", -3.5]
representsabs(-3.5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import json
import sys
def do_abs(args):
assert len(args) == 1
val = do(args[0])
return abs(val)
def do_add(args):
assert len(args) == 2
left = do(args[0])
right = do(args[1])
return left + right
def do(expr):
# Integers evaluate to themselves.
if isinstance(expr, int):
return expr
# Lists trigger function calls.
assert isinstance(expr, list)
if expr[0] == "abs":
return do_abs(expr[1:])
if expr[0] == "add":
return do_add(expr[1:])
assert False, f"Unknown operation {expr[0]}"
def main():
assert len(sys.argv) == 2
with open(sys.argv[1], "r") as reader:
program = json.load(reader)
result = do(program)
print(f"=> {result}")
if __name__ == "__main__":
main()
Example Usage
- Create a file
expr.tll
with the content:
1
["add", ["abs", -3], 2]
- Run the interpreter:
1
python expr.py expr.tll
- Output:
1
=> 5
Let’s Expand on Dynamic Dispatch and Recursion
Dynamic Dispatch
Dynamic dispatch is the process where the program decides at runtime which function to call based on the type or value of the arguments. In our interpreter, the do
function decides which specific function to call (do_add
or do_abs
) based on the first element of the list.
Recursion
Recursion occurs when a function calls itself, either directly or indirectly. It’s powerful for solving problems by breaking them into smaller, similar subproblems.
In our interpreter:
do
callsdo_add
do_add
callsdo
again to evaluate its arguments
This continues recursively until the base case (an integer) is reached.
For example, evaluating the expression ["abs", ["add", 1, 2]]
involves:
do
callsdo_abs
with["add", 1, 2]
do_abs
callsdo
with["add", 1, 2]
do
callsdo_add
with[1, 2]
do_add
callsdo
on1
and2
(both integers, returned as is)- Sum
1 + 2 = 3
, return3
todo_abs
do_abs
returnsabs(3)
, which is3
Introduction to Variables in Interpreter
To handle variables, we use an environment: a dictionary that stores variable names as keys and their values. This environment acts as the context in which expressions are evaluated.
We modify functions to accept the environment as a parameter so they can read and update variables.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def do_get(env, args):
assert len(args) == 1
assert isinstance(args[0], str)
assert args[0] in env, f"Unknown variable {args[0]}"
return env[args[0]]
def do_set(env, args):
assert len(args) == 2
assert isinstance(args[0], str)
value = do(env, args[1])
env[args[0]] = value
return value
def do(env, expr):
if isinstance(expr, int):
return expr
assert isinstance(expr, list)
if expr[0] == "abs":
return do_abs(env, expr[1:])
if expr[0] == "add":
return do_add(env, expr[1:])
if expr[0] == "get":
return do_get(env, expr[1:])
if expr[0] == "set":
return do_set(env, expr[1:])
assert False, f"Unknown operation {expr[0]}"
Streamlining Function Calls with a Lookup Table
Using many if
statements to choose the function to call can become unwieldy. We can automate this by creating a lookup table that maps operation names to their functions using introspection:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
OPS = {
name.replace("do_", ""): func
for (name, func) in globals().items()
if name.startswith("do_")
}
def do(env, expr):
if isinstance(expr, int):
return expr
assert isinstance(expr, list)
assert expr[0] in OPS, f"Unknown operation {expr[0]}"
func = OPS[expr[0]]
return func(env, expr[1:])
Benefits of Using Introspection
- Reduced Complexity:
do
function is simpler and easier to read. - Scalability: Adding new operations only requires defining a new
do_*
function. - Reliability: Automatically includes all
do_*
functions, reducing human error.
Potential Downsides
- Can be harder to understand for beginners.
- Operations are not explicitly listed; you must check the code to see all available operations.
Explicit Lookup Table (Alternative)
If introspection feels obscure, define the lookup table explicitly:
1
2
3
4
5
6
OPS = {
"abs": do_abs,
"add": do_add,
"get": do_get,
"set": do_set,
}
Conclusion
This chapter introduced interpreters — programs that translate code line by line during execution — and contrasted them with compilers, which translate entire programs upfront.
We built a simple expression interpreter capable of handling basic mathematical operations, explored dynamic dispatch (selecting which function to run based on input), and implemented recursion (functions calling themselves to break problems down).
We also extended the interpreter with variables using an environment dictionary to store and retrieve values, then improved code maintainability by using introspection for operation dispatch.
Understanding interpreters helps grasp how programming languages execute and how language design supports flexibility and ease of development — key knowledge for becoming a proficient programmer.
In the next post, we’ll continue exploring advanced software design strategies explained in this book. Stay tuned!
Please go ahead and read the whole book Software Design by Example here. This is just a track of my learning journey as I explore the concepts.