Session 24
Programs with State
An Exercise with "O"
Suppose that we know how to multiply and divide whole numbers only by 2, but not by any other number. That's not all that crazy... In hardware, multiplying and dividing by two are primitive shift operations; all other multiplications and divisions require significant work.
If we have two arbitrary integers, m and n, and one of them is a power of two, we can multiply them by taking advantage of this simple fact:
m * n = half(m) * double(n)
If we halve-and-double recursively, eventually m becomes 1 and n becomes the product we seek.
(multiply m n)
that
multiplies in this way, displaying its arguments as it goes along.
Assume that m
is a power of 2. For example:
> (multiply 16 43) 16 43 ; printed by the function 8 86 4 172 2 344 1 688 688 ; the value of the call
You may find useful the Racket primitives display
,
newline
, and begin
from your
the reading
for today, or even the displayln
I implemented there.
Solutions to the Exercise
Check out
this solution,
for which I wrote a custom display-mn
function. I
also wrote half
and double
functions, to
make the code read cleaner and to ensure that I don't use
*
anywhere in my function.
(define multiply (lambda (m n) (display-line m n) (if (= m 1) n (multiply (half m) (double n)))))
Notice that I didn't have to use the begin
special
form, because lambda
allows multiple expressions
implicitly. You also saw that in your reading for today.
There are at least two ways I can improve this solution, one substantive and one cosmetic:
- First, it assumes that m is always a power of 2. I would like to handle the general case where m is odd or becomes odd along the way.
- Second, I'm fussy about unaligned output. I like to mind my p's and q's — and my m's and n's!
We can handle odd values for m with one little trick: Use integer division, so that m always remains an integer. Whenever we encounter an odd m, including m = 1, add the value of n to an accumulator variable. Stop when m = 0.
Racket's quotient
function does integer division for
us.
We can create prettier output by using some of Racket's other I/O primitives.
This implementation makes both improvements.
-
It defines
(multiply m n)
as an interface procedure that calls a local recursive function to do the work. The helper uses an accumulator variable to sum up all the n's that it loses when it encounters an odd m. -
The utility function
(pad n)
creates equal-sized fields to display integers, which is called by a Racket function that behaves like a Python format string and looks a lot like a C format statement.pad
itself uses keyword arguments similar to what you see in languages like Python.
> (multiply 32 473) ; one addition to acc > (multiply 31 473) ; one addition to acc on every pass
This old algorithm is not just an academic exercise. At the
machine level, doubling an integer is the same as a shifting the
integer left by one bit, and halving is the same as a one-bit
right shift. Shift operations are much faster than the
more complex multiplication operation, so compilers try to use them
whenever they can. To demonstrate this, in Version 2, I show how
you can redefine half
and double
to use
Racket's
arithmetic-shift
operator. Between this operator and the use of tail recursion,
our solution is quite efficient!
Shifting Gears: Data Abstractions Involving State
The day has finally arrived.
Up to this point, our discussion of data abstractions has had the
same character as the rest of the course. Every data value has
been immutable, that is, unchangeable. Even when we
created local "variables" with let
, we assigned a
value to each variable exactly once: at the time it was
created. The value of the "variable" never varied.
But we know that most languages provide data items that are mutable, whose values can be changed. They really are variable. Indeed, the programming styles with which you are most familiar — procedural and object-oriented — are based in large part on the idea of mutable data. How is this idea implemented in a programming language?
At times, some of you have been frustrated with Racket because it seems to lack several features that you rely on in writing programs in other languages: statement sequencing, I/O operations, and variables. These features all belong to a style of writing programs called imperative programming.
This name comes from the idea that programs using such features treat the computer as a data storehouse. The program's job is to tell the computer what to do, to go through a sequence of state changes. In this style, the primary purpose of a program expression is not its value but some side effect.
Obviously, there are some situations in which writing programs in a purely functional style is awkward, or even seemingly impossible. So, even Racket's minimalist ancestor Scheme provides a few essential imperative features: procedures and special forms whose purposes are not to compute values but rather to generate side effects. These include I/O operations that can read and write data from standard input and from files. Racket I/O is based on the the idea of a port, which behaves much like a stream in Java or C++.
... revisit these expressions that demonstrate simple I/O in Racket
Racket itself provides many more I/O options, including C-style
format strings. I used printf
in
my second solution
to the Russian peasant multiplication exercise.
Some programming languages allow only a functional style; they provide no imperative operators at all. The best example of a pure functional language these days is Haskell. Such languages do allow input/output operations, but only through operators that compute values and preserve the notions of function and value. (If only we had more time.... ™)
Our attention now turns to the idea of mutable data in Racket and in programming languages more generally.
Today, we quickly introduce some of Racket's imperative features for sequencing expressions and mutating data. We consider these in the context implementing data abstractions, which can hide the imperative details of the implementation from the client code. As we do, we will continue to consider the implications of these features for language interpreters, especially how they might represent such data and how they might interpret imperative expressions.
Input/output and sequences of statements are topics with which you have much experience, so we do not need to spend a lot of time on them. For that reason, I gave away the secret by asking you to read a mini-lecture on some of Racket's imperative features for today's session.
Sequencing
Most programming languages provide a way for the programmer to tell a program the order in which to execute a set of statements.
Up to now, the only sequences we have seen in our programs have been sequences of arguments being passed to functions. We have not cared in what order Racket evaluated arguments, because none of the expressions had any side effects; order did not matter. That was a good thing, because it allowed us to focus on the value of the function and not on lower-level machine issues.
(We did learn that and
and or
do
short-circuit evaluation, so we know they work left to right. But
they are special forms, with their own evaluation rules.)
What's more, we did not have a good way to tell which order Racket used. If every expression simply returns a value, how can we tell which value is produced first?
Evaluating expressions in a particular order can affect us in other ways, too. One of the greatest barriers to parallel programming on a large scale is the artificial ordering of expressions that we programmers introduce into our solutions. Parallel programmers have to derive solutions in which order matters as little as possible, so that tasks can be assigned to independent processors whenever they become available. Functional programming style "parallelizes" much more naturally than imperative programming.
Digression. For an example of a parallel functional language aimed at high-performance numerical computing, see the programming language Sisal, either at its SourceForge page or in this tutorial.
However, when you need to guarantee that events happen in a certain order, you need a way to indicate that order. Even in Racket.
In Pascal and its descendants, including Ada, the
begin..end
construct indicates a sequence of
statements to be executed in order. In Java and C, the
{..}
play the same role. You can almost think of
these syntactic structures as high-level operators that
tell a program to execute a sequence of statements in a particular
order.
In Racket, we can introduce order in several ways. The first is
the special form begin
, which you read about
for today:
(begin <expr_1> ... <expr_n>)
The begin
expression guarantees that Racket will
execute the expressions inside it in order.
For more on begin
, revisit
these expressions
from the reading that demonstrate sequencing in Racket.
Quick Exercise: Now that we have seen some of Racket's imperative features, write an expression that tells us whether Racket evaluates function arguments left-to-right or right-to-left.
With begin
and display
, we now have a way
to determine the order in which Racket evaluates arguments to
procedures:
> (+ (begin (display "left ") 1)
(begin (display "right ") 2))
left right 3
So, now we know.
Why haven't we taken advantage of this ability to sequence operations before? Because we have not used any operators with side effects before today!
Sequencing only matters when expressions have side effects.
Unless you are using mutable data or input/output expressions, you do not need sequences of expressions. When writing programs in a functional style, we had no need for sequencing. Indeed, the presence of a sequence of statements in a functional program is usually a sign of an error.
The converse is also true:
Side effects only matters when expressions are in sequence.
Unless you are using a sequences of expressions, what is the point of changing the value of a variable? No one can see the effect!
Variable Assignment and Sharing
All of the programs we have written thus far have had the form of a "black box" with inputs and a single 'output' value. The value of the output depended only on the values of the inputs passed to the function, not on any previous value. These programs employed no notion of state at all.
That is what makes it functional programming. All interactions between functions occur by passing arguments. This makes it easier to write, reason about, and modify individual pieces of code. For those of you experienced in imperative programming, though, functional programming seems to make it more difficult to write whole programs, at least until you become comfortable with the new way of thinking.
To this point in the course, we have thought of a name being associated with a value. For some applications, though, we naturally think of a data object changing values over time. To think about a variable object, we need to think of a name being associated with a location that holds a value. We can't change the link from the variable's name to the location, but we can change what is stored there.
Your reading for next time will explore the relationship among name, value, and location a bit more.
Let's consider how Racket implements mutable data, both as an example of what is essential and with an eye toward how we can implement mutable data in a language interpreter.
Changing the Value of a Data Object: withdraw
Suppose that we wanted to build a model of a bank account. We would like to be able to withdraw funds from the account, so we would include withdrawal as one of the operations on our bank account ADT. We might try something like this:
> (define balance 100) > (define withdraw (lambda (amount) (if (>= balance amount) ;; balance is a free variable!! (begin (define balance (- balance amount)) balance) (error "Insufficient funds" balance)))) define: not allowed in an expression context in: (define balance (- balance amount))
Side note: Many of you noticed in your readings at the start of the course that Racket allowsdefine
expressions at the top level of alambda
body. When used that way, they behave roughly like aletrec
. At the time, you did not have a deep enough knowledge of Racket to understand when you could use an internaldefine
and when you couldn't, so I outlawed it. Also, we were trying to learn functional programming style, and thelet
expression gave us all that we needed.
But Racket screams at us. When the interpreter encounters a
define
expression inside the definition of
withdraw
, it faces a couple of tough decisions. Are
we creating a new name, or are we changing the value of an
existing name? If we are creating a new name, do we intend it
to be a globally-accessible name or a locally-accessible one?
If we intend for the new definition to be global, then we are
overwriting the old value, which in effect makes the definition a
value change. If we intend it as a local definition, then the
effect of withdraw
will not be visible outside of the
function, and the global value of balance
will remain
100.
Racket avoids the confusion by flagging such a use of
define
as an error. This requires us to create a
local binding using more traditional means, say, a let
expression, to accomplish the task.
In many ways, this approach helps us as programmers. By removing
the semantic ambiguity that a nested define
can cause,
Racket simplifies learning the language and using it to write
programs:
Defining a name and changing the value of a named object are different activities and so ought to be different operations in the language.
Programming languages that use the same name or symbol for two different ideas lead to code that confuses both the writer of the code and the reader.
This is a common source of difficulty for people learning to program in C++. Consider these two cases:
Foo a = new Foo(); || Foo a; || a = new Foo();
Foo a
creates and initializes an object. The
=
statement on Line 2 assigns a value to an
object. The example on the right creates two Foo
s!
Racket was designed to keep separate ideas separate. It includes
a special form named set!
for changing the
value of a data object that has already been defined. Note that
the name of this operator ends with an exclamation mark. This
naming convention tells us that the operator modifies one of its
arguments, usually the first. We have seen and used a similar
convention for naming predicates, whose names end with question
marks.
Operators such as set!
are called mutators,
because they change the value of something. Computer scientists
usually pronounce the exclamation mark as "bang". So, we call
the operation set!
"set bang". (Here is a bit on
origin of this usage.)
As you may have seen already, Racket provides other mutators for
specific data types, such as vector-set!
for changing
a value inside a vector. It even provides set-car!
and set-cdr!
. (!) But don't try them on
regular cons
cells. They operate only on
mutable pairs,
which are created by the function mcons
.
To see how set!
works, let's re-define
withdraw
to work using set!
:
> (define balance 100) > (define withdraw (lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) (error "Insufficient funds" balance))))
This works just fine, and withdraw
now behaves as we
had hoped. The only problem with this definition is that the
balance is external to the definition of withdraw
,
which means that other functions can also modify its
value.
> (withdraw 20) ;; I need a little cash.
80
> balance
80
> (withdraw 40)
40
> (embezzle 30) ;; Hey! What's going on here??
> (withdraw 40) ;; I need a little more cash, but...
Insufficient funds: 10
We know that letting an ADT reveal its implementation can have bad side effects (see our discussion from last time), but this is the worst possible way. How can we prevent all access to the balance of a bank account from any other function in the world?
Using Closures to Encapsulate Data Objects
We could try to solve this problem by making balance
internal to the withdraw
function using a
let
expression:
> (define withdraw (lambda (amount) (let ((balance 100)) (if (>= balance amount) ;; now balance is bound (begin (set! balance (- balance amount)) balance) (error "Insufficient funds" balance))))) > (withdraw 20) 80 > (embezzle 50) ;; Whatever it does ... ... > (withdraw 20) ;; ... it doesn't touch my balance! 80 ;; But neither does another call to withdraw. > (withdraw 60) ;; withdraw always starts at $100. 40
Whoa! By treating balance
as a local variable in this
way, each call to withdraw
begins with a balance of
$100. That may make the bank's clients happy, but probably not the
bank!
Using let
inside the lambda
expression,
we have clobbered the idea that the account balance persists over
time. Each call creates a new local variable, with the same
initial balance. One way to solve this problem is to create the
balance
outside of the withdraw
function itself by making the let
the body of the
define
:
> (define withdraw
(let ((balance 100))
(lambda (amount)
(if (>= balance amount) ;; balance is bound
(begin
(set! balance (- balance amount))
balance)
(error "Insufficient funds" balance)))))
> (withdraw 20)
80
> (withdraw 100)
Insufficient funds: 80
This works, but why? Because the lambda
expression
that is withdraw
was created in an
environment in which balance
exists and has an initial
value of 100. Each call to the function named withdraw
refers to the existing balance
object, whatever its
current value. But withdraw
does not create
balance
, so no local definition shadows the
(relatively) global one.
The idea underlying this technique is called a closure. We
write a function (here, withdraw
) that refers to a
free variable that wasn't free at the time the
lambda
was created. The function carries with it
a reference to the data items that exist when it is created, so
every evaluation of the function refers to this set of
variable/value bindings.
The set of variable/value bindings is called an environment.
By carrying its environment with it, the function bound to
withdraw
can access and modify the same data object
over an extended number of uses. The closure allows the data to
persist over time, as long as the function that refers to it
exists.
More Than One Customer
We are getting close to a satisfactory solution. One last problem remains. Our definition "hard-wires" the initial bank account balance as 100. We probably do not want this to be the case, because different customers will want to open accounts with different initial balances. So we re-write our definition one more time, creating a constructor for bank accounts that support withdrawals:
> (define make-withdraw (lambda (balance) (lambda (amount) (if (>= balance amount) ;; balance is still bound, but (begin ;; to a new object on each call! (set! balance (- balance amount)) balance) (error "Insufficient funds" balance)))))
We call our new function make-withdraw
, and it takes
a single argument, the initial account balance. Evaluating
(make-withdraw 60)
returns a withdrawal function that
works on a new bank account balance with an initial value
of 60. Each call to make-withdraw
creates a new
formal parameter and produces a new function that is, in effect,
a different bank account.
We might use make-withdraw
in this way:
> (define account-for-eugene (make-withdraw 100)) ;;; eugene is poor
> (account-for-eugene 20) ;;; withdraw $20
80
> (define account-for-bill (make-withdraw 400000000)) ;;; bill is rich
> (account-for-bill 20) ;;; withdraw $20
399999980
> (account-for-eugene 20) ;;; withdraw $20
60
> (account-for-eugene 120) ;;; withdraw $120
Insufficient funds: 60
> (account-for-bill 120) ;;; withdraw $120
399999860
This way of using of a closure gives us more flexibility. We write
a function (here, make-withdraw
) that receives an
argument (here, the initial balance). The formal parameter of
make-withdraw
creates a new balance
object, which is referred to by the free variable in the
lambda
that is returned as make-withdraw
's
answer. This allows make-withdraw
to create distinct
functions (for example, account-for-eugene
and
account-for-bill
) that modify unique data objects.
In effect, make-withdraw
acts like a
constructor for a new object. We can also say that
make-withdraw
is a factory method.
Actually... We have already used the idea of a factory method to create a closure this semester, albeit without mutable data. Think back to when we first learned about higher-order functions in Session 5. In your reading assignment that day, I created a generator of verification functions for self-verifying numbers:
(define make-validator (lambda (f m) (lambda (list-of-digits) (zero? (modulo (apply + (counted-map f list-of-digits)) m)))))
make-validator
is a factory method that creates
validator functions. It creates a function in an environment
where f
and m
are bound, and then returns
the function as its answer. It never changes
f
or m
, but it does continue to use them.
Then, consider our examples of curried functions in the reading for Session 6. They were based on the idea of creating a new function that has access to the original argument:
(define curried-add (lambda (x) (lambda (y) (+ x y))))
In fact, we used a closure every time we created a function that returned another function. Sometimes ideas aren't so scary if we treat them as matter of fact!
Wrap Up
-
Reading
- Review these lecture notes, especially the last two sections, Changing the Value of a Data Object and Using Closures to Encapsulate Data Objects. Work through the code in today's zip file.
- Prepare for next time by reading a short section on denoted and expressed values.
-
Homework
- Homework 9 is available and due on Monday. Homework 10 will become available the next day. Homework 9 is the beginning of a three-part project, so get a good start on it now!