Post

Unlocking Software Extensibility – Part 3

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.

Unlocking Software Extensibility – Part 3

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.

Function Evaluation

One way to evaluate the design of a piece of software is to ask how extensible it is, i.e., how easily we can add or change things. For our interpreter from Part 2, adding features is easy, but the language it interprets doesn’t let users define new operations and thereby, we need to let users create and call functions. To make function calls work the way most programmers expect, it’s crucial to implement scope properly. This ensures that the parameters and variables used within a function aren’t confused with those defined outside it, effectively preventing name collisions. Understanding how function evaluation works, particularly the difference between eager and lazy evaluation, is essential for writing efficient and effective code. Here’s a concise comparison:

Eager or Lazy Evaluation?

  • Eager Evaluation: Expressions are evaluated as soon as they are bound to a variable or passed as an argument.
  • Lazy Evaluation: Expressions are only evaluated when their values are needed, deferring computation until the last moment.
Evaluation Type Example Languages Advantage Disadvantage
Eager Evaluation C, Java, Ruby, JavaScript - Simplified reasoning
- Improved efficiency
- Facilitates parallelism
- May waste resources
- Can cause side effects
- Limited expressiveness
Lazy Evaluation Haskell, Scala, Clojure, R - Avoids unnecessary work
- Saves memory
- Enables infinite data structures
- Introduces overhead
- Complicates debugging
- Unpredictable performance

Environment and Call Stack

Think of the environment as a place where your program keeps track of all variables and their values.

Now, imagine a stack of plates. Each plate represents a set of variables active during a specific function call. When you call a function, you add a new plate on top. When the function ends, you remove that plate. Each plate (or stack frame) contains variables and their values for one specific function call.


Scopes

Dynamic Scoping

Dynamic scoping means you search for variable values by looking through the stack of plates (stack frames) from the topmost plate to the bottom.

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
# This is our stack of plates (list of dictionaries)
environment = []

# Function to add a new plate (stack frame)
def push_frame():
    environment.append({})

# Function to remove the topmost plate (stack frame)
def pop_frame():
    environment.pop()

# Function to set a variable in the topmost plate
def set_variable(name, value):
    environment[-1][name] = value

# Function to get a variable's value, searching from the topmost plate down
def get_variable(name):
    for frame in reversed(environment):
        if name in frame:
            return frame[name]
    raise NameError(f"Variable {name} not found")

# Example usage
push_frame()  # We enter a new function call, add a new plate
set_variable('x', 10)  # Put x on the topmost plate
print(get_variable('x'))  # Look for x starting from the topmost plate; Output: 10
pop_frame()  # We leave the function call, remove the topmost plate

Lexical Scoping

Lexical scoping means you determine which variables you can use based on where they are written in the code, not based on the stack of plates.

  • It doesn’t matter how or when inner is called; it will always refer to x from outer.
  • The inner function knows about x because x is defined in the code block where inner is also defined.
1
2
3
4
5
6
7
def outer():
    x = 10  # x is defined in the outer function
    def inner():
        print(x)  # inner function can see x because of lexical scoping
    inner()

outer()  # Output: 10

Key Difference

  • Dynamic Scoping: Searches for variables by looking through all the active function calls.
  • Lexical Scoping: Determines which variables are accessible based on where they are written in the source code.

Function Definition

When you define a function, you specify its parameters and its body. The function definition is stored as a list with the keyword func, followed by the parameters and the body.

1
2
3
4
5
def do_func(env, args):
    assert len(args) == 2  # Ensure there are exactly two arguments: params and body
    params = args[0]       # First argument: list of parameter names
    body = args[1]         # Second argument: the body of the function (a code block)
    return ["func", params, body]  # Return a list representing the function

Function Call

When you call a function, you pass arguments to it. The function executes its body using these arguments. Here’s how you set up and execute a function call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def do_call(env, args):
    # Set up the call.
    assert len(args) >= 1  # Ensure at least one argument: the function name
    name = args[0]         # The name of the function to call
    values = [do(env, a) for a in args[1:]]  # Evaluate arguments for the function call

    # Find the function.
    func = env_get(env, name)  # Retrieve function definition from environment
    assert isinstance(func, list) and (func[0] == "func")  # Ensure it's a function
    params, body = func[1], func[2]  # Extract parameters and body
    assert len(values) == len(params)  # Arguments count matches parameters

    # Run in a new environment.
    env.append(dict(zip(params, values)))  # Create new stack frame with params and values
    result = do(env, body)                  # Execute function body
    env.pop()                              # Remove the stack frame after execution

    # Report.
    return result  # Return the result of function execution

Unpacking One Line

Let’s break down this line:

1
env.append(dict(zip(params, values)))
  • zip(params, values) pairs each parameter with its corresponding argument value.
  • dict(zip(params, values)) creates a dictionary from those pairs.
  • env.append(...) adds this dictionary as a new stack frame on the environment stack.

Real-World Analogy

  • Defining a Function: Imagine writing a recipe with ingredients (parameters) and steps (body). You save it in a recipe book (lookup table).
  • Calling a Function: When you cook (call the function), you gather the ingredients (arguments), look up the recipe, create a workspace (stack frame), follow the steps (execute the body), then clean up (pop the stack frame).

Function Within Functions

Normally, we define functions at the top level, but Python allows nested functions. Inner functions can access variables from their outer function.


Returning Inner Functions

Functions are just another kind of data in Python, so you can return a function from another function.

Closures

A closure is when an inner function captures variables from its enclosing function.

Why Use Closures?

  • Data Privacy: Variables captured by the inner function are not accessible from outside.
  • Function Customization: Closures let functions remember data and use it later.

Closures as Objects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def make_object(initial_value):
    private = {"value": initial_value}

    def getter():
        return private["value"]

    def setter(new_value):
        private["value"] = new_value

    return {"get": getter, "set": setter}

obj = make_object(0)
print("initial value", obj["get"]())  # Output: 0
obj 
print("object now contains", obj["get"]())  # Output: 99

Conclusion

This chapter explored function evaluation from a software design perspective. Functions promote modularity and reusability. By understanding concepts like scope and environment management, designers can ensure clear variable access and efficient function calls.

Additionally, closures provide a powerful pattern for data privacy and function customization, enabling well-structured and maintainable programs.

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.

This post is licensed under CC BY 4.0 by the author.