Chapter 1: Building Abstractions with Functions by Harold Abelson, Gerald Jay Sussman, Julie Sussman - HTML preview

PLEASE NOTE: This is an HTML preview only and some elements such as links or page numbers may be incorrect.
Download the book in PDF, ePub, Kindle for a complete version.

the fifth. Yet translating the equation into a traditional computer language would force us to choose one of the

quantities to be computed in terms of the other four. Thus, a function for computing the pressure could not be

used to compute the temperature, even though the computations of both quantities arise from the same equation.

In this section, we sketch the design of a general model of linear relationships. We define primitive con-

straints that hold between quantities, such as an adder(a, b, c) constraint that enforces the mathematical

relationship a + b = c.

We also define a means of combination, so that primitive constraints can be combined to express more complex

relations. In this way, our program resembles a programming language. We combine constraints by constructing

a network in which constraints are joined by connectors. A connector is an object that “holds” a value and may

participate in one or more constraints.

For example, we know that the relationship between Fahrenheit and Celsius temperatures is:

9 * c = 5 * (f - 32)

This equation is a complex constraint between c and f. Such a constraint can be thought of as a network

consisting of primitive adder, multiplier, and constant constraints.

In this figure, we see on the left a multiplier box with three terminals, labeled a, b, and c. These connect the

multiplier to the rest of the network as follows: The a terminal is linked to a connector celsius, which will

hold the Celsius temperature. The b terminal is linked to a connector w, which is also linked to a constant box

that holds 9. The c terminal, which the multiplier box constrains to be the product of a and b, is linked to the c

terminal of another multiplier box, whose b is connected to a constant 5 and whose a is connected to one of the

terms in the sum constraint.

Computation by such a network proceeds as follows: When a connector is given a value (by the user or by a

constraint box to which it is linked), it awakens all of its associated constraints (except for the constraint that just

awakened it) to inform them that it has a value. Each awakened constraint box then polls its connectors to see if

there is enough information to determine a value for a connector. If so, the box sets that connector, which then

awakens all of its associated constraints, and so on. For instance, in conversion between Celsius and Fahrenheit,

w, x, and y are immediately set by the constant boxes to 9, 5, and 32, respectively. The connectors awaken

the multipliers and the adder, which determine that there is not enough information to proceed. If the user (or

some other part of the network) sets the celsius connector to a value (say 25), the leftmost multiplier will be

awakened, and it will set u to 25 * 9 = 225. Then u awakens the second multiplier, which sets v to 45, and

v awakens the adder, which sets the fahrenheit connector to 77.

Using the Constraint System. To use the constraint system to carry out the temperature computation outlined

above, we first create two named connectors, celsius and fahrenheit, by calling the make_connector

constructor.

>>> celsius = make_connector(’Celsius’)

>>> fahrenheit = make_connector(’Fahrenheit’)

Then, we link these connectors into a network that mirrors the figure above. The function make_converter

assembles the various connectors and constraints in the network.

31

>>> def make_converter(c, f):

"""Connect c to f with constraints to convert from Celsius to Fahrenheit."""

u, v, w, x, y = [make_connector() for _ in range(5)]

multiplier(c, w, u)

multiplier(v, x, u)

adder(v, y, f)

constant(w, 9)

constant(x, 5)

constant(y, 32)

>>> make_converter(celsius, fahrenheit)

We will use a message passing system to coordinate constraints and connectors. Instead of using functions

to answer messages, we will use dictionaries. A dispatch dictionary will have string-valued keys that denote the

messages it accepts. The values associated with those keys will be the responses to those messages.

Constraints are dictionaries that do not hold local states themselves. Their responses to messages are non-pure

functions that change the connectors that they constrain.

Connectors are dictionaries that hold a current value and respond to messages that manipulate that value.

Constraints will not change the value of connectors directly, but instead will do so by sending messages, so that

the connector can notify other constraints in response to the change. In this way, a connector represents a number,

but also encapsulates connector behavior.

One message we can send to a connector is to set its value. Here, we (the ’user’) set the value of celsius

to 25.

>>> celsius[’set_val’](’user’, 25)

Celsius = 25

Fahrenheit = 77.0

Not only does the value of celsius change to 25, but its value propagates through the network, and so the

value of fahrenheit is changed as well. These changes are printed because we named these two connectors

when we constructed them.

Now we can try to set fahrenheit to a new value, say 212.

>>> fahrenheit[’set_val’](’user’, 212)

Contradiction detected: 77.0 vs 212

The connector complains that it has sensed a contradiction: Its value is 77.0, and someone is trying to set it

to 212. If we really want to reuse the network with new values, we can tell celsius to forget its old value:

>>> celsius[’forget’](’user’)

Celsius is forgotten

Fahrenheit is forgotten

The connector celsius finds that the user, who set its value originally, is now retracting that value, so

celsius agrees to lose its value, and it informs the rest of the network of this fact. This information eventually

propagates to fahrenheit, which now finds that it has no reason for continuing to believe that its own value is

77. Thus, it also gives up its value.

Now that fahrenheit has no value, we are free to set it to 212:

>>> fahrenheit[’set_val’](’user’, 212)

Fahrenheit = 212

Celsius = 100.0

This new value, when propagated through the network, forces celsius to have a value of 100. We have

used the very same network to compute celsius given fahrenheit and to compute fahrenheit given

celsius. This non-directionality of computation is the distinguishing feature of constraint-based systems.

Implementing the Constraint System. As we have seen, connectors are dictionaries that map message names

to function and data values. We will implement connectors that respond to the following messages:

32

• connector[’set_val’](source, value) indicates that the source is requesting the connector

to set its current value to value.

• connector[’has_val’]() returns whether the connector already has a value.

• connector[’val’] is the current value of the connector.

• connector[’forget’](source) tells the connector that the source is requesting it to forget its

value.

• connector[’connect’](source) tells the connector to participate in a new constraint, the source.

Constraints are also dictionaries, which receive information from connectors by means of two messages:

• constraint[’new_val’]() indicates that some connector that is connected to the constraint has a

new value.

• constraint[’forget’]() indicates that some connector that is connected to the constraint has for-

gotten its value.

When constraints receive these messages, they propagate them appropriately to other connectors.

The adder function constructs an adder constraint over three connectors, where the first two must add to

the third: a + b = c. To support multidirectional constraint propagation, the adder must also specify that it

subtracts a from c to get b and likewise subtracts b from c to get a.

>>> from operator import add, sub

>>> def adder(a, b, c):

"""The constraint that a + b = c."""

return make_ternary_constraint(a, b, c, add, sub, sub)

We would like to implement a generic ternary (three-way) constraint, which uses the three connectors and

three functions from adder to create a constraint that accepts new_val and forget messages. The response

to messages are local functions, which are placed in a dictionary called constraint.

>>> def make_ternary_constraint(a, b, c, ab, ca, cb):

"""The constraint that ab(a,b)=c and ca(c,a)=b and cb(c,b) = a."""

def new_value():

av, bv, cv = [connector[’has_val’]() for connector in (a, b, c)]

if av and bv:

c[’set_val’](constraint, ab(a[’val’], b[’val’]))

elif av and cv:

b[’set_val’](constraint, ca(c[’val’], a[’val’]))

elif bv and cv:

a[’set_val’](constraint, cb(c[’val’], b[’val’]))

def forget_value():

for connector in (a, b, c):

connector[’forget’](constraint)

constraint = {’new_val’: new_value, ’forget’: forget_value}

for connector in (a, b, c):

connector[’connect’](constraint)

return constraint

The dictionary called constraint is a dispatch dictionary, but also the constraint object itself. It responds

to the two messages that constraints receive, but is also passed as the source argument in calls to its connectors.

The constraint’s local function new_value is called whenever the constraint is informed that one of its

connectors has a value. This function first checks to see if both a and b have values. If so, it tells c to set its

value to the return value of function ab, which is add in the case of an adder. The constraint passes itself

(constraint) as the source argument of the connector, which is the adder object. If a and b do not both have

values, then the constraint checks a and c, and so on.

If the constraint is informed that one of its connectors has forgotten its value, it requests that all of its connectors

now forget their values. (Only those values that were set by this constraint are actually lost.)

A multiplier is very similar to an adder.

33

>>> from operator import mul, truediv

>>> def multiplier(a, b, c):

"""The constraint that a * b = c."""

return make_ternary_constraint(a, b, c, mul, truediv, truediv)

A constant is a constraint as well, but one that is never sent any messages, because it involves only a single

connector that it sets on construction.

>>> def constant(connector, value):

"""The constraint that connector = value."""

constraint = {}

connector[’set_val’](constraint, value)

return constraint

These three constraints are sufficient to implement our temperature conversion network.

Representing connectors. A connector is represented as a dictionary that contains a value, but also has

response functions with local state. The connector must track the informant that gave it its current value, and

a list of constraints in which it participates.

The constructor make_connector has local functions for setting and forgetting values, which are the re-

sponses to messages from constraints.

>>> def make_connector(name=None):

"""A connector between constraints."""

informant = None

constraints = []

def set_value(source, value):

nonlocal informant

val = connector[’val’]

if val is None:

informant, connector[’val’] = source, value

if name is not None:

print(name, ’=’, value)

inform_all_except(source, ’new_val’, constraints)

else:

if val != value:

print(’Contradiction detected:’, val, ’vs’, value)

def forget_value(source):

nonlocal informant

if informant == source:

informant, connector[’val’] = None, None

if name is not None:

print(name, ’is forgotten’)

inform_all_except(source, ’forget’, constraints)

connector = {’val’: None,

’set_val’: set_value,

’forget’: forget_value,

’has_val’: lambda: connector[’val’] is not None,

’connect’: lambda source: constraints.append(source)}

return connector

A connector is again a dispatch dictionary for the five messages used by constraints to communicate with

connectors. Four responses are functions, and the final response is the value itself.

The local function set_value is called when there is a request to set the connector’s value. If the connector

does not currently have a value, it will set its value and remember as informant the source constraint that

requested the value to be set. Then the connector will notify all of its participating constraints except the constraint

that requested the value to be set. This is accomplished using the following iterative function.

>>> def inform_all_except(source, message, constraints):

34

"""Inform all constraints of the message, except source."""

for c in constraints:

if c != source:

c[message]()

If a connector is asked to forget its value, it calls the local function forget-value, which first checks to

make sure that the request is coming from the same constraint that set the value originally. If so, the connector

informs its associated constraints about the loss of the value.

The response to the message has_val indicates whether the connector has a value. The response to the

message connect adds the source constraint to the list of constraints.

The constraint program we have designed introduces many ideas that will appear again in object-oriented

programming. Constraints and connectors are both abstractions that are manipulated through messages. When

the value of a connector is changed, it is changed via a message that not only changes the value, but validates

it (checking the source) and propagates its effects (informing other constraints). In fact, we will use a similar

architecture of dictionaries with string-valued keys and functional values to implement an object-oriented system

later in this chapter.

2.5 Object-Oriented Programming

Object-oriented programming (OOP) is a method for organizing programs that brings together many of the ideas

introduced in this chapter. Like abstract data types, objects create an abstraction barrier between the use and

implementation of data. Like dispatch dictionaries in message passing, objects respond to behavioral requests.

Like mutable data structures, objects have local state that is not directly accessible from the global environment.

The Python object system provides new syntax to ease the task of implementing all of these useful techniques for

organizing programs.

But the object system offers more than just convenience; it enables a new metaphor for designing programs

in which several independent agents interact within the computer. Each object bundles together local state and

behavior in a way that hides the complexity of both behind a data abstraction. Our example of a constraint

program began to develop this metaphor by passing messages between constraints and connectors. The Python

object system extends this metaphor with new ways to express how different parts of a program relate to and

communicate with each other. Not only do objects pass messages, they also share behavior among other objects

of the same type and inherit characteristics from related types.

The paradigm of object-oriented programming has its own vocabulary that reinforces the object metaphor. We

have seen that an object is a data value that has methods and attributes, accessible via dot notation. Every object

also has a type, called a class. New classes can be defined in Python, just as new functions can be defined.

2.5.1 Objects and Classes

A class serves as a template for all objects whose type is that class. Every object is an instance of some particular

class. The objects we have used so far all have built-in classes, but new classes can be defined similarly to how

new functions can be defined. A class definition specifies the attributes and methods shared among objects of that

class. We will introduce the class statement by revisiting the example of a bank account.

When introducing local state, we saw that bank accounts are naturally modeled as mutable values that have a

balance. A bank account object should have a withdraw method that updates the account balance and returns

the requested amount, if it is available. We would like additional behavior to complete the account abstraction:

a bank account should be able to return its current balance, return the name of the account holder, and accept

deposits.

An Account class allows us to create multiple instances of bank accounts. The act of creating a new object

instance is known as instantiating the class. The syntax in Python for instantiating a class is identical to the syntax

of calling a function. In this case, we call Account with the argument ’Jim’, the account holder’s name.

>>> a = Account(’Jim’)

An attribute of an object is a name-value pair associated with the object, which is accessible via dot notation.

The attributes specific to a particular object, as opposed to all objects of a class, are called instance attributes.

Each Account has its own balance and account holder name, which are examples of instance attributes. In the

broader programming community, instance attributes may also be called fields, properties, or instance variables.

35

>>> a.holder

’Jim’

>>> a.balance

0

Functions that operate on the object or perform object-specific computations are called methods. The side

effects and return value of a method can depend upon, and change, other attributes of the object. For example,

deposit is a method of our Account object a. It takes one argument, the amount to deposit, changes the

balance attribute of the object, and returns the resulting balance.

>>> a.deposit(15)

15

In OOP, we say that methods are invoked on a particular object. As a result of invoking the withdraw

method, either the withdrawal is approved and the amount is deducted and returned, or the request is declined and

the account prints an error message.

>>> a.withdraw(10)

# The withdraw method returns the balance after withdrawal

5

>>> a.balance

# The balance attribute has changed

5

>>> a.withdraw(10)

’Insufficient funds’

As illustrated above, the behavior of a method can depend upon the changing attributes of the object. Two

calls to withdraw with the same argument return different results.

2.5.2 Defining Classes

User-defined classes are created by class statements, which consist of a single clause. A class statement defines

the class name and a base class (discussed in the section on Inheritance), then includes a suite of statements to

define the attributes of the class:

class <name>(<base class>):

<suite>

When a class statement is executed, a new class is created and bound to <name> in the first frame of the

current environment. The suite is then executed. Any names bound within the <suite> of a class statement,

through def or assignment statements, create or modify attributes of the class.

Classes are typically organized around manipulating instance attributes, which are the name-value pairs as-

sociated not with the class itself, but with each object of that class. The class specifies the instance attributes of

its objects by defining a method for initializing new objects. For instance, part of initializing an object of the

Account class is to assign it a starting balance of 0.

The <suite> of a class statement contains def statements that define new methods for objects of that

class. The method that initializes objects has a special name in Python, __init__ (two underscores on each side

of “init”), and is called the constructor for the class.

>>> class Account(object):

def __init__(self, account_holder):

self.balance = 0

self.holder = account_holder

The __init__ method for Account has two formal parameters. The first one, self, is bound to the newly

created Account object. The second parameter, account_holder, is bound to the argument passed to the

class when it is called to be instantiated.

The constructor binds the instance attribute name balance to 0. It also binds the attribute name holder

to the value of the name account_holder. The formal parameter account_holder is a local name to

the __init__ method. On the other hand, the name holder that is bound via the final assignment statement

persists, because it is stored as an attribute of self using dot notation.

Having defined the Account class, we can instantiate it.

36

>>> a = Account(’Jim’)

This “call” to the Account class creates a new object that is an instance of Account, then calls the con-

structor function __init__ with two arguments: the newly created object and the string ’Jim’. By convention,

we use the parameter name self for the first argument of a constructor, because it is bound to the object being

instantiated. This convention is adopted in virtually all Python code.

Now, we can access the object’s balance and holder using dot notation.

>>> a.balance

0

>>> a.holder

’Jim’

Identity. Each new account instance has its own balance attribute, the value of which is independent of other

objects of the same class.

>>> b = Account(’Jack’)

>>> b.balance = 200

>>> [acc.balance for acc in (a, b)]

[0, 200]

To enforce this separation, every object that is an instance of a user-defined class has a unique identity. Object

identity is compared using the is and is not operators.

>>> a is a

True

>>> a is not b

True

Despite being constructed from identical calls, the objects bound to a and b are not the same. As usual,

binding an object to a new name using assignment does not create a new object.

>>> c = a

>>> c is a

True

New objects that have user-defined classes are only created when a class (such as Account) is instantiated

with call expression syntax.

Methods. Object methods are also defined by a def statement in the suite of a class statement. Below,

deposit and withdraw are both defined as methods on objects of the Account class.

>>> class Account(object):

def __init__(self, account_holder):

self.balance = 0

self.holder = account_holder

def deposit(self, amount):

self.balance = self.balance + amount

return self.balance

def withdraw(self, amount):

if amount > self.balance:

return ’Insufficient funds’

self.balance = self.balance - amount

return self.balance

While method definitions do not differ from function definitions in how they are declared, method definitions

do have a different effect. The function value that is created by a def statement within a class statement is

bound to the declared name, but bound locally within the class as an attribute. That value is invoked as a method

using dot notation from an instance of the class.

Each method definition again includes a special first parameter self, which is bound to the object on which

the method is invoked. For example, let us say that deposit is invoked on a particular Account object and

37

passed a single argument value: the amount deposited. The object itself is bound to self, while the argument is

bound to amount. All invoked methods have access to the object via the self parameter, and so they can all

access and manipulate the object’s state.

To invoke these methods, we again use dot notation, as illustrated below.

>>> tom_account = Account(’Tom’)

>>> tom_account.deposit(100)

100

>>> tom_account.withdraw(90)

10

>>> tom_account.withdraw(90)

’Insufficient funds’

>>> tom_account.holder

’Tom’

When a method is invoked via dot notation, the object itself (bound to tom_account, in this case) plays a

dual role. First, it determines what the name withdraw means; withdraw is not a name in the environment,

but instead a name that is local to the Account class. Second, it is bound to the first parameter self when the

withdraw method is invoked. The details of the procedure for evaluating dot notation follow in the next section.

2.5.3 Message Passing and Dot Expressions

Methods, which are defined in classes, and instance attributes, which are typically assigned in constructors, are

the fundamental elements of ob