We now turn our attention to procedural abstraction, a strategy for
decomposing complex programs into smaller pieces of code in the form
of functions (also called procedures or subroutines; there are
subtle differences in how these terms are used in various contexts,
but for our purposes, we will treat them as synonyms). A function
encapsulates some computation behind an interface, and as with any
abstraction, the user of a function need only know what the function
does and not how it accomplishes it. A function also generalizes
computation by taking in arguments that affect what it computes. The
result of the computation is the function’s return value.
In this unit, we start by introducing Scheme, a functional language in
the Lisp family. We then discuss aspects of functions that are
relevant to all procedural languages before proceeding to take a
closer look at functional programming, a programming paradigm that
models computation after mathematical functions.
In this section, we introduce a high-level programming language that
encourages a functional style. Our object of study, the R5RS Scheme
language, employs a very similar model of computation to Python’s,
but uses only expressions (no statements) and specializes in symbolic
computation.
Scheme is a dialect of Lisp, the
second-oldest programming language that is still widely used today
(after Fortran). The
community of Lisp programmers has continued to thrive for decades, and
new dialects of Lisp such as Clojure have some of the fastest
growing communities of developers of any modern programming language.
To follow along with the examples in this text, you can download a
Scheme interpreter or use an online interpreter.
Scheme programs consist of expressions, which are either simple
expressions or combinations in the form of lists. A simple
expression consists of a literal or a symbol. A combination is a
compound expression that consists of an operator expression followed
by zero or more operand sub-expressions. Both the operator and
operands are contained within parentheses:
>(quotient102)5
Scheme exclusively uses prefix notation. Operators are often symbols,
such as + and *. Compound expressions can be nested, and they
may span more than one line:
Evaluating a combination requires first examining the operator to see
if it represents a special form[1], which has its own evaluation
procedure. If the operator is not a special form, then the operator
and operand expressions are evaluated in some arbitrary order. The
function that is the value of the operator is then applied to the
arguments that are the values of the operands.
The if expression in Scheme is an example of a special form. While
it looks syntactically like a call expression, it has a different
evaluation procedure. The general form of an if expression is:
(if<predicate><consequent><alternative>)
To evaluate an if expression, the interpreter starts by evaluating
the <predicate> part of the expression. If the <predicate>
evaluates to a true value, the interpreter then evaluates the
<consequent> and returns its value. Otherwise it evaluates the
<alternative> and returns its value. The <alternative> may be
elided.
Numerical values can be compared using familiar comparison operators,
but prefix notation is used in this case as well:
>(>=21)#t
Truth values in Scheme, including the boolean values #t (for true)
and #f (for false), can be combined with boolean special forms,
which have evaluation procedures as follows:
(and<e1>...<en>) The interpreter evaluates the expressions
<e> one at a time, in left-to-right order. If any <e>
evaluates to a false value, the value of the and expression is
that false value, and the rest of the <e>’s are not evaluated.
If all <e>’s evaluate to true values, the value of the and
expression is the value of the last one.
(or<e1>...<en>) The interpreter evaluates the expressions
<e> one at a time, in left-to-right order. If any <e>
evaluates to a true value, that value is returned as the value of
the or expression, and the rest of the <e>’s are not
evaluated. If all <e>’s evaluate to false values, the value of
the or expression is the value of the last one.
Truth values can also be manipulated with the not procedure:
(not<e>) The value of a not expression is #t when the
expression <e> evaluates to a false value, and #f
otherwise.
Values can be named using the define special form:
>(definepi3.14)>(*pi2)6.28
New functions (usually called procedures in Scheme) can be defined
using a second version of the define special form. For example, to
define squaring, we write:
(define(squarex)(*xx))
The general form of a procedure definition is:
(define(<name><formalparameters>)<body>)
The <name> is a symbol to be associated with the procedure
definition in the environment. The <formalparameters> are the
names used within the body of the procedure to refer to the
corresponding arguments of the procedure. The <body> is an
expression that will yield the value of the procedure application when
the formal parameters are replaced by the actual arguments to which
the procedure is applied. The <name> and the <formalparameters> are grouped within parentheses, just as they would be in
an actual call to the procedure being defined.
Having defined square, we can now use it in call expressions:
Scheme supports local function definitions with static scope. We will
defer discussion of this until we cover higher-order functions.
Anonymous functions, also called lambda functions, are created using
the lambda special form. A lambda is used to create a
procedure in the same way as define, except that no name is
specified for the procedure:
(lambda(<formal-parameters>)<body>)
The resulting procedure is just as much a procedure as one that is
created using define. The only difference is that it has not been
associated with any name in the environment. In fact, the following
expressions are equivalent:
Pairs are built into the Scheme language. For historical reasons,
pairs are created with the cons built-in function (and thus, pairs
are also known as cons cells), and the elements of a pair are
accessed with car and cdr:
>(definex(cons12))>x(1.2)>(carx)1>(cdrx)2
Figure 20 illustrates the pair structure created by
(cons12).
Recursive lists are also built into the language, using pairs. A
special value denoted '() represents the empty list. A
recursive list value is rendered by placing its elements within
parentheses, separated by spaces:
Figure 21 demonstrates that the structure
corresponding to the list whose text representation is (1234)
consists of a sequence of pairs, terminated by the empty list
(represented in the diagram as a box containing the symbol
\(\varnothing\)).
A sequence of pairs that is terminated by something other than the
empty list is called an improper list. Such a list is rendered by
the interpreter with a dot preceding the last element; the result of
(cons12) is an example, as shown above, consisting of just a
single pair in the sequence. The following demonstrates a more complex
sequence:
>(cons1(cons2(cons34)))(123.4)
Figure 22 demonstrates the pair structure
corresponding to the improper list above.
The illustrations in Figure 21 and
Figure 22 demonstrate that pairs and other
compound objects have reference semantics – the cdr part of a pair stores a reference to the
next pair in the sequence. The following code further demonstrates
these reference semantics with variables:
> (define x (cons 1 2))
> (define y x)
> (eqv? x y)
#t
> (set-car! y 7)
> x
(7 . 2)
Here, the definition (defineyx) results in both x and y
referring to the same pair object. The eqv? procedure when applied
to pairs returns true only when the two arguments refer to the same
pair object (as opposed to equal?, which compares pairs
structurally). Furthermore, when we use the set-car! mutator to
modify the first item of the pair referenced by y, we can see that
x references the same pair since it too shows the modification.
Whether an object is the empty list can be determined using the
primitive null? predicate. Using it, we can define the standard
sequence operations for computing the length of a proper list and
selecting elements:
All the compound data objects we have used so far were constructed
ultimately from numbers. One of Scheme’s strengths is working with
arbitrary symbols as data.
In order to manipulate symbols we need a new element in our language:
the ability to quote a data object. Suppose we want to construct the
list (ab). We can’t accomplish this with (listab), because
this expression constructs a list of the values of a and b
rather than the symbols themselves. In Scheme, we refer to the symbols
a and b rather than their values by preceding them with a
single quotation mark:
In Scheme, any expression that is not evaluated is said to be
quoted. This notion of quotation is derived from a classic
philosophical distinction between a thing, such as a dog, which runs
around and barks, and the word “dog” that is a linguistic construct
for designating such things. When we use “dog” in quotation marks, we
do not refer to some dog in particular but instead to a word. In
language, quotation allow us to talk about language itself, and so it
is in Scheme:
>(list'define 'list)(definelist)
Quotation also allows us to type in compound objects, using the
conventional printed representation for lists. We have already seen
that '() denotes an empty list. Here are other examples:
>(car'(a b c))a>(cdr'(a b c))(bc)
Quotation in Scheme is distinct from strings: the latter represent
raw, unstructured data in character format, while the former
represents structured data:
>"(- 3)";astringcontainingthecharacters#\( #\- #\space #\3 #\)"(- 3)">'(- 3) ; produces a list containing the symbol - and number 3(-3)>(car'(- 3))->(cdr'(- 3))(3)>(-3);callsthe-procedureonthenumber3-3
In the examples above, the string literal "(-3)" evaluates to
itself. The quoted expression '(-3) evaluates to a list
containing the symbol - as its first element and the number 3 as
its second. The last example evaluates the symbol - to obtain the
corresponding procedure, evaluates the number 3 to itself, and then
calls the - procedure on the number 3, producing -3. Put
another way, data in a string literal remains as character data,
neither evaluated nor parsed. A quoted expression is parsed but not
evaluated, producing a structured representation of the data. An
unquoted expression is both parsed and evaluated by the interpreter.
The full Scheme language contains additional features, such as
mutation operations, vectors, and maps. However, the subset we have
introduced so far provides a rich functional programming language
capable of implementing many of the ideas we have discussed so far.
We first consider various schemes that are used for passing data to
functions in the form of parameters and arguments. We make a
distinction between the parameters that appear in a function
definition, which are also called formal parameters, and the actual
values that are passed to the function when it is called. The latter
are often called actual parameters, but we will use the term
argument to refer to these values and the shorthand parameter for
formal parameters.
Some languages allow, or even require, parameter names to be provided
when calling a function. This strategy is called named parameters or
keyword arguments.
Keyword arguments generally allow arguments to be provided in a
different order than the parameter list of a function. In Python, for
example, a keyword argument can be used for any parameter. Consider
the following code:
deffoo(x,y):print(x,y)
Calling foo() without keyword arguments passes the first argument
as the first parameter, and the second argument as the second
parameter:
>>> foo(1,2)1 2
However, the arguments can be reordered using the parameter names:
>>> foo(y=1,x=2)2 1
Python also provides mechanisms for defining parameters to be
positional-only or
keyword-only, but we will not
discuss these mechanisms here.
A handful of languages require names to be provided for all or most
arguments by default, as well as requiring that they be given in the
same order as the parameters. The following is an example in Swift 3:
Calling greet() with the arguments in reverse order is erroneous.
Swift is also rare in that it allows different argument and parameter
names to be specified for a parameter. This means that the name
provided for an argument when calling a function can differ from the
internal name of the parameter used in the body of the function.
In some languages, a function declaration or definition may be
provided with a default argument value that allows the function to
be called without that argument. This can be an alternative to
overloading, where separate function definitions are written to handle
the cases where an argument is present or missing.
The following is an example in Python:
defpower(base,exponent=2):returnbase**exponent
The power() function can be called with a single argument, in
which case the default argument 2 is used to compute the square of
the number. It can also be called with two arguments to compute an
arbitrary power:
>>> power(3)9>>> power(3,4)81
Parameters that have default arguments generally must appear at the
end of the parameter list. Languages differ on when and in which
environment they evaluate the default argument. The most common
strategy is to evaluate a default argument every time a function is
called, but to do so in the definition environment (static scope).
Python is rare in that it only evaluates default arguments once, when
the function definition statement is executed. This means that if the
value of the parameter is modified in the function, subsequent calls
to the same function could have different default values for the same
parameter. For example:
deftest(x=[]):x.append(1)print(x)test()test()
This will print:
[1][1,1]
C and C++ have numerous rules concerning default arguments,
necessitated by the fact that an entity can be declared multiple
times. Default arguments can be provided in both stand-alone
declarations as well as definitions. However, it is illegal for
multiple visible declarations of the same entity to provide a default
argument for the same parameter, even if the provided value is the
same. The set of default arguments is the union of all visible
declarations within the same scope, and a declaration may only
introduce a default argument for a parameter if all following
parameters have been supplied with default arguments by the previous
and current declarations. Names used in a default argument are
resolved at the point of declaration, but the argument expressions are
evaluated when the function is called.
The following is a legal example of multiple declarations in C++:
A language may provide a mechanism for a function to be called with a
variable number of arguments. This feature is often referred to as
varargs, and functions that make use of it are variadic. The
mechanism may provide type safety, or it may permit unsafe uses that
result in erroneous or undefined behavior. A variadic parameter
generally must appear at the end of a parameter list, and it matches
arguments that remain once the non-variadic parameters are matched.
Usually, only a single variadic parameter is allowed.
In languages that provide safe variadic functions, a common mechanism
for doing so is to automatically package variable arguments into a
container, such as an array or tuple. For example, the following
Python function computes the product of its arguments:
The * in front of a parameter name indicates a variadic parameter,
and the variable arguments are passed as a tuple bound to that name.
The function above iterates over the elements of the tuple, updating
the total product. In order to call product(), 0 or more arguments
must be provided:
>>> product()1>>> product(1,2,3)6
Python also provides variadic keyword arguments, which are packaged
into a dictionary. Placing ** in front of a parameter specifies
that it is a variadic keyword parameter, and such a parameter must be
the last one. As an example, the following function has both a
non-keyword variadic parameter and a variadic keyword parameter,
printing out the tuple corresponding to the former and the dictionary
for the latter:
Finally, Python allows a sequence or dictionary to be “unpacked” using
the * or ** operator, allowing the unpacked values to be used
where a list of values is required. For example, the following unpacks
a list to make a call to product():
>>> product(*[1,2,3])6
Scheme also supports variadic arguments. A procedure can be defined to
take variadic arguments by using an improper list as the parameter
list, terminated by a symbol rather than an empty list. The variadic
parameter binds to any number of arguments, packaged into a (proper)
list:
>(define(func.args)args)>(func)()>(func123)(123)
The procedure func takes in any number of arguments and returns
the list containing those arguments. It thus behaves as the built-in
list procedure. We can also define a procedure that takes in both
required and variadic arguments, as in the following definition of
average:
The procedure takes in one or more arguments, with the first bound to
the parameter x and the rest encapsulated in a list bound to the
variadic nums parameter. We can forward variadic arguments using
apply, which takes a procedure, any number of regular arguments,
and lastly, a list containing the remaining arguments. For example,
(apply+12'(34)) is equivalent to the call (+1234). In
the first example using average above, nums is bound to an
empty list in the call (average1), and (apply+xnums) is
equivalent to (apply+1'()), which itself is equivalent to (+1). In the third example, nums is bound to a list (357),
so (apply+xnums) is equivalent to (apply+1'(357)),
which is in turn equivalent to (+1357).
In both Python and Scheme, a variadic parameter can match arguments
with any type because the two languages are dynamically typed. In
statically typed languages, however, variadic parameters are usually
restricted to a single type, though that type may be polymorphic. For
example, the following is a variadic method in Java:
The arguments to print_all() must be Strings, and they are
packaged into a String array. Java also allows a single String
array to be passed in as an argument:
C and C++ also have a mechanism for variadic arguments, but it poses
significant safety issues. In particular, it provides no information
about the number of arguments and their types to the function being
called. The following is an example of a function that returns the
sum of its arguments:
In this function, the first argument is assumed to be the number of
remaining arguments, and the latter are assumed to have type int.
Undefined behavior results if either of these conditions is violated.
Another strategy is to use a format string to determine the number and
types of arguments, as used in printf() and similar functions. The
lack of safety of variadic arguments enables vulnerabilities such as
format string attacks.
C++11 provides variadic templates that
are type safe. We will discuss them later in the text.
Another area in which languages differ is in the semantics and
mechanism used in order to communicate arguments between a function
and its caller. A function parameter may be unidirectional (used for
only passing input to a function or only passing output from a
function to its caller), or it may be bidirectional. These cases are
referred to as input, output, and input/output parameters. A
language need not support all three parameter categories.
Different parameter passing techniques, or call modes, are used by
languages. These affect the semantics of arguments and parameters as
well as what parameter categories are supported. The following are
specific call modes used by different languages:
Call by value. A parameter represents a new variable in the frame
of a function invocation. The argument value is copied into the
storage associated with the new variable. Call-by-value parameters
only provide input to a function, as in the following example in
C++:
voidfoo(intx){x++;cout<<x<<endl;}intmain(){inty=3;foo(y);// prints 4cout<<y<<endl;// prints 3}
Even though foo() modifies the input value, the modified value
is not propagated back to the caller.
Call by reference. An l-value must be passed as the argument, as
the parameter aliases the object that is passed in. Any
modifications to the parameter are reflected in the argument object.
Thus, call by reference parameters provide both input and output. In
C++, reference parameters provide call by reference, and they may be
restricted to just input by declaring them const[2]. The
following C++ example uses call by reference to swap the values of
two objects:
voidswap(int&x,int&y){inttmp=x;x=y;y=tmp;}intmain(){intx=3,y=4;swap(x,y);cout<<x<<" "<<y<<endl;// prints 4 3}
Call by reference is sometimes used to refer to passing objects
indirectly using pointers. The following C/C++ function swaps object
values using pointers:
voidswap(int*x,int*y){inttmp=*x;*x=*y;*y=tmp;}intmain(){intx=3,y=4;swap(&x,&y);printf("%d %d\n",x,y);// prints 4 3}
Technically speaking, however, the arguments and parameters are
separate pointer objects that are passed by value. That being said,
the effect emulates call by reference, enabling both input and
output to be achieved through a parameter.
Call by result. In this mode, a parameter represents a new
variable that is not initialized with a value from the caller.
Instead, the caller specifies an l-value for the argument, and when
the function call terminates, the final value of the parameter is
copied to the l-value. Thus, call by result only provides output
parameters. The following is an example, using C-like syntax with
call by result:
voidfoo(resultintx){x=3;x++;// x is now 4}inty=5;foo(y);// y is now 4print(y);// prints 4
Call by value-result. This is the combination of call by value and
call by result. The argument value is copied into a new variable
corresponding to the parameter, and then upon return from the
function, the value of the parameter is copied back to the l-value
provided by the caller. This differs from call by reference in that
copies are made upon entry and exit to the function. This can be
illustrated by passing the same l-value to multiple parameters, as
in the following example using C-like syntax with call by
value-result:
intfoo(value-resultintx,value-resultinty){x++;returnx-y;}intz=3;print(foo(z,z));// prints 1print(z);// prints 3 or 4, depending on the semantics
In this code, x and y are new variables that are initialized
to the value of z, i.e. 3. The increment of x does not
affect y, since they are separate variables, so the call to
foo() returns 1. Thus, 1 is printed. (The final value of z
depends on the semantics of the language as to whether it is copied
from x or y.) If call by reference were used instead, then
x and y would alias the same object, and the call to
foo() would return 0.
Call by name. In this mode, a full expression can be provided as
an argument, but it is not evaluated at the time a function is
called. Instead, the parameter name is replaced by the expression
where the name occurs in the function, and the expression is
evaluated at the time that it is encountered in the body. This is a
form of lazy evaluation, where a value is not computed until it is
needed. The following is an example using C-like syntax with call by
name:
voidfoo(nameinta,nameintb){print(b);// becomes print(++y)print(b);// becomes print(++y)}intx=-1,y=3;foo(++x,++y);// prints 4, then 4 or 5 depending on the exact// language semantics; y is now 4 or 5print(x);// prints -1 -- x is unchanged
In this example, the argument expression ++x is never evaluated
since the corresponding call-by-name parameter a is never used.
On the other hand, the expression ++y is computed since the
corresponding parameter b does get used. Depending on the
language semantics, the expression may only be evaluated once and
the value cached for subsequent use, or it may be evaluated each
time the parameter is used.
There is a subtle issue that arises in call by name. Consider the
following code that uses C-like syntax with call by name:
If we replace the occurrence of the parameter x in bar()
with the argument expression, we get y+1+y as the argument
to print(). If this is evaluated in the environment of
bar(), the result is 7. This is undesirable, since it means that
the implementation detail of a local declaration of y changes
the behavior of the function.
Instead, the argument expression should be evaluated in the
environment of caller. This requires passing both the argument and
its environment to the function invocation. Languages that use call
by name often use a compiler-generated local function, called a
thunk, to encapsulate the argument expression and its environment.
This thunk is then passed to the invoked function, and it is the
thunk that is called when the parameter is encountered.
In some languages, the expression corresponding to a call-by-name
parameter is only evaluated the first time the parameter is
referenced, caching the result. The cached result is then used in
each subsequent occurrence of the parameter.
Call by value is the call mode used by most modern languages,
including C, C++ (for non-reference parameters), Java, Scheme, and
Python. Programmers often mistakenly believe the latter three
languages use call by reference, but in reality, they combine call by
value with reference semantics. This combination is sometimes called
call by object reference. The following example illustrates that
Python is call by value:
defswap(x,y):tmp=xx=yy=tmp
>>> x,y=1,2>>> swap(x,y)>>> x,y(1, 2)
The erroneous swap() function merely changes the values of the
local variables, which changes the objects they refer to, without
affecting the variables used as arguments. This demonstrates that the
storage for the global x and y is distinct from that of the
parameters, so Python does not use call by reference. In fact, Python
cannot even emulate call by reference in the manner that C and C++
pointers do.
We proceed to summarize to evaluation process of a function call.
The first step is to determine the non-local environment of a call to
a nested function. In languages with nested functions and static
scope, a reference to the non-local environment is stored in the
associated function object when the nested-function definition itself
is executed. Under dynamic scope with deep binding, the non-local
environment is determined when the function is referenced by name.
Finally, in dynamic scope with shallow binding, the non-local
environment is the environment that is active when the function is
called.
The next step is to pass the arguments to the function, using a newly
created activation record for the function call. The arguments are
evaluated in the existing environment and passed to the callee as
follows:
Call by value and call by value-result: the argument is
evaluated to obtain its r-value. The r-value is copied into the
storage for the corresponding parameter in the new activation
record.
Call by reference: the argument is evaluated to obtain its
l-value. The corresponding parameter is bound to the object
associated with the l-value.
Call by result: the argument is evaluated to obtain its
l-value. Storage is allocated but not initialized within the new
activation record.
Call by name: the argument expression is packaged into a thunk
with the current environment. The parameter is bound to a reference
to the thunk.
Once the parameters have been passed, execution of the caller pauses,
and the body of the callee is executed in an environment consisting of
the newly created activation record along with the callee’s non-local
environment. For call by name, an occurrence of a call-by-name
parameter invokes the corresponding thunk either the first time the
parameter is named or every time, according to the semantics of the
language.
When the called function returns, its return value, if there is one,
is placed in a designated storage location, generally in the
activation record of the caller. For a call-by-result or
call-by-value-result parameter, the current r-value of the parameter
is copied into the object associated with the l-value of the
corresponding function-call argument. The activation record for the
callee is then destroyed, and execution resumes in the caller at the
point following the function call. The evaluation result of the
function call itself is the return value of the function.
Recursion is a mechanism for repetition that makes use of functions
and function application. It involves a function calling itself
directly or indirectly, usually with arguments that are in some sense
“smaller” than the previous arguments. A recursive computation
terminates when it reaches a base case, an input where the result
can be computed directly without making any recursive calls.
It is sufficient for a language to provide recursion and conditionals
in order for it to be Turing complete.
On a machine, recursion works due to the fact that each invocation of
a function has its own activation record that maps its local variables
to values. Consider the following recursive definition of factorial:
Calling factorial(4) results in five invocations of
factorial(), with arguments from 4 down to 0. Each has its own
activation record with its own binding for the parameter n:
Figure 23 is an illustration of the set of activation
records as produced by Python Tutor.
When n is looked up while executing the body of factorial(),
each invocation obtains its own value of n without being affected
by the other activation records.
An activation record requires more than just storage for parameters
and local variables in order for function invocation to work.
Temporary values also need to be stored somewhere, and since each
invocation needs its own storage for temporaries, they are generally
also placed in the activation record. An invocation also needs to know
where to store its return value, usually in temporary storage in the
frame of the caller. Finally, a function needs to know how to return
execution to its caller. Details are beyond the scope of this text,
but included in this information is the instruction address that
follows the function call in the caller and the address of the
caller’s activation record.
The set of temporary objects can be conservatively determined
statically, so the size of an activation record, as well as the
placement of objects within it, can be determined at compile time. For
factorial() above, temporary storage is required for n-1 as
well as the result of the recursive call to factorial(). The
location of the latter in the caller is used by a recursive call to
store its return value. Depending on the implementation, the
invocation of factorial(0) may still have space for these
temporary objects in its activation record even though they will not
be used.
A recursive computation uses a separate activation record for each
call to a function. The amount of space required to store these
records is proportional to the number of active function calls. In
factorial(n) above, when the computation reaches factorial(0),
all n+1 invocations are active at the same time, requiring space
in O(n). Contrast this with the following iterative implementation
that uses constant space:
The space requirements of the recursive version of factorial(),
however, is not intrinsic to the use of recursion but is a result of
how the function is written. An invocation of factorial(k) cannot
complete until the recursive call to factorial(k-1) does, since
it has to multiply the result by k. The fact that the invocation
has work that needs to be done after the recursive call requires its
activation record to be retained during the recursive call, leading to
the linear space requirement.
Consider an alternative recursive computation of factorial:
Observe that the factorial_tail() function does not do any work
after the completion of its recursive call. This means that it no
longer needs the storage for parameters, local variables, or temporary
objects when the recursive call is made. Furthermore, since
factorial(n,k) directly returns the result of the recursive call
factorial(n-1,n*k), the latter can store its return value in
the location meant for the return value of factorial(n,k) in the
caller of factorial(n,k), and it can return execution directly to
that caller. Thus, an optimizing implementation can reuse the space
for the activation record of factorial_tail(n,k) for
factorial_tail(n-1,n*k) since the activation record of the
former is no longer required.
This process can be generalized to any function call, not just
recursive calls. A function call is a tail call if its caller
directly returns the value of the call without performing any
additional computation. A function is tail recursive if all of its
recursive calls are tail calls. Thus, factorial_tail() is tail
recursive.
A tail-recursive computation uses only a constant number of activation
records, so its space usage matches that of an equivalent iterative
computation. In fact, many functional languages do not provide
constructs for iteration, since they can be expressed equivalently
using tail recursion. These languages often require that
implementations perform tail-call optimization, reusing the space
for activation records where possible.
Since a tail call requires that no computation be performed after it
returns, calls that syntactically appear to be tail calls may not be
when implicit computation may occur at the end of a function. A
specific example of this is scope-based resource management, as in the
example below:
While it appears that this code does not do computation after the
recursive call, the local vector<int> object has a destructor that
must run after the recursive call completes. Thus, the recursive call
to sum() is not a tail call, and this computation is not tail
recursive.
Another situation that prevents tail-call optimization is when a
function contains a function definition within it, in languages that
use static scope and support the full power of higher-order functions.
The nested function requires access to its definition environment, so
that environment must be retained if the nested function can be used
after the invocation of its enclosing function completes or within a
tail call.
Iteration and recursion are equivalent in power and are just different
tools for expressing the same algorithms. In fact, an iterative
implementation can be converted to a tail-recursive one in a fairly
mechanical manner, with local variables becoming function parameters.
For instance, consider the Fibonacci sequence, which is defined via
the following recurrence:
\[\begin{split}F(n) = \begin{cases}
n ~&\text{if $n = 0$ or $n = 1$}\\
F(n-1) + F(n-2) ~&\text{otherwise}
\end{cases}\end{split}\]
The following is an iterative implementation that uses a “bottom-up”
approach to compute an element of the sequence:
Unlike a naive recursive implementation that takes time exponential in
\(n\) and linear space, this iterative version takes linear time
and constant space. We can translate the iterative implementation to a
recursive one by introducing additional function parameters to replace
each of the local variables, resulting in the following:
We use default arguments to provide the variables their initial
values. The termination conditions are the same as the iterative
version: when n==0 for the base case, and when i==n for
the recursive case. And the variable updates are the same, just that
the new values are passed to a recursive call rather than modifying
the existing variables. The resulting function is tail-recursive, and
with tail-call optimization, it is exactly equivalent to the iterative
version.
A recursive implementation can also be translated to an iterative one,
though the equivalent iterative version may require an explicit data
structure to replace the implicit storage provided by the call stack.
As an example, the following is a recursive function that computes
the sum of the elements contained in a binary tree:
This is a tree-recursive function, and it makes use of a call stack
that is linear with respect to the height of the tree. An equivalent
iterative implementation needs to provide this storage manually:
The initial value for sum is the same as the return value for the
base case in the recursive version. In both implementations, if the
current tree is non-empty, its datum is added to the sum, and the
computation is repeated on each child.
Since either iteration or recursion can express the same algorithm, if
both are available in a programming system, it is up to the programmer
which one to use. A programmer may decide that one tool is a more
natural fit for a particular algorithm, or they may consider which one
their system can optimize better. Ideally, the programmer is adept at
both tools and makes the decision based on which is more appropriate
for the use case rather than which one they are more comfortable
utilizing.
Recall that a first-class entity is one that supports the operations
that can be done on other entities in a language, including being
passed as a parameter, returned from a function, and created
dynamically. In a language in which functions are first class, it is
possible to write higher-order functions that take in another
function as a parameter or return a function. Other languages may also
support higher-order functions, even if functions are not first-class
entities that can be created at runtime.
In some languages, it is possible to define objects that aren’t
functions themselves but provide the same interface as a function.
These are known as function objects or functors. In general,
languages enable functors to be written by allowing the function-call
operator to be overloaded. Consider the following example in C++:
The Counter class implements a functor that returns how many times
it has been called. Multiple Counter objects can exist
simultaneously, each with their own count:
Countercounter1,counter2;cout<<counter1()<<endl;// prints 0cout<<counter1()<<endl;// prints 1cout<<counter1()<<endl;// prints 2cout<<counter2()<<endl;// prints 0cout<<counter2()<<endl;// prints 1cout<<counter1()<<endl;// prints 3
Functors allow multiple instances of a function-like object to exist,
each with their own state that persists over the lifetime of the
functor. This is in contrast to functions, where automatic objects do
not persist past a single invocation, and static objects persist over
the entire program execution.
Python also allows functors to be written by defining the special
__call__ method:
In general, additional parameters can be specified when overloading
the function-call operator, emulating functions that can take in those
arguments.
Some languages do not allow the function-call operator itself to be
overloaded but specify conventions that allow functor-like objects to
be defined and used. For example, the following is an implementation
of Counter in Java using the Supplier<T> interface, which
specifies a zero-argument method that produces a T:
This functor-like object is then invoked by explicitly calling the
get() method:
Supplier<Integer>counter1=newCounter();Supplier<Integer>counter2=newCounter();System.out.println(counter1.get());// prints 0System.out.println(counter1.get());// prints 1System.out.println(counter1.get());// prints 2System.out.println(counter2.get());// prints 0System.out.println(counter2.get());// prints 1System.out.println(counter1.get());// prints 3
As another example, the Predicate interface in Java is implemented
by functor-like objects that take in an argument and return a boolean
value:
A higher-order function may take another function as a parameter. We
first examine languages that only have top-level functions and allow a
pointer or reference to a function to be passed as an argument. We
then examine how passing a function as an argument can affect the
environment in which the function’s code is executed.
In some languages, functions can be passed as parameters or return
values but cannot be created within the context of another function.
In these languages, all functions are defined at the top level, and
only a pointer or reference to a function may be used as a value.
Consider the following example in C, a language that provides
function pointers:
The apply() function takes in an array, its size, and a pointer to
a function that takes in an int and returns an int. It applies
the function to each element in the array, replacing the original
value with the result. The add_one() function is passed as an
argument to apply() (C automatically converts a function to a
function pointer), and the result is that each element in A has
been incremented.
In the code above, there are three environments associated with the
add_one() function: its definition environment, the environment
where it was referenced (in main()), and the environment where it
was called (in apply()). Depending on the semantics of the
language, any of these three environments may be components of the
environment in which the body of add_one() is executed.
Recall that in static scope, the code in a function has access to the
names in its definition environment, whereas in dynamic scope, it has
access to the names in the environment of its use. Considering dynamic
scope, is the non-local environment of a function the one where the
function was referenced or the one where it was called? The following
is an example where this distinction is relevant:
In dynamic scope, a function has access to the environment of its use.
In the example above, however, the result is different depending on if
the use environment of baz() is where the function was referenced
or where it was called. In the former case, the non-local environment
of baz() is the environment of main(), and the x in the
body of baz() would refer to the one defined in main(). This
is known as deep binding. In the latter case, the non-local
environment of baz() is the environment of foo(), and x in
baz() would refer to the one defined in foo(). This is called
shallow binding. Both approaches are valid, and the binding policy
of a language determines which one is used.
Binding policy can also make a difference when static scope is used in
the case of functions defined locally inside of a recursive function.
However, deep binding is universally used in languages with static
scope, so that the environment established at the time of a function’s
definition is the one the function has access to.
A key feature of functional programming is the ability to define a
function from within another function, allowing the dynamic creation
of a function. In languages with static scoping, such a nested
function has access to its definition environment, and the combination
of a function and its definition environment is called a closure.
Variables used in the nested function but defined in the enclosing
environment are said to be captured by the closure. If a nested
function is returned or otherwise leaks from the enclosing function,
the environment of the enclosing function generally must persist after
the function returns, since bindings within it may be accessed by the
nested function.
As an example, consider the following higher-order function in Python
that returns a nested function:
The make_greater_than() function takes in a threshold value and
constructs a nested function that determines if its input is greater
than the threshold value. The threshold variable is located in the
activation record of make_greater_than() but is captured by
greater_than(). Since the latter is returned, the activation
record must persist so that invocations of greater_than() can
access the binding for threshold.
Observe that each time make_greater_than() is called, a different
instance of greater_than() is created with its own enclosing
environment. Thus, different invocations of make_greater_than()
result in different functions:
The parent frame of the invocation is that in which threshold is
bound to 3, so x>threshold evaluates to false.
Languages that are not purely functional may allow modification of a
captured variable. For example, the following defines a data
abstraction for a bank account using nested functions:
The nonlocal statements are required in Python, since it assumes
that assignments are to local variables by default. We can then use
the created functions as follows:
A common pattern in Python is to transform a function (or class) by
applying a higher-order function to it. Such a higher-order function
is called a decorator, and Python has specific syntax for decorating
functions:
The decorated function’s definition is executed normally, and then the
decorator is called on the function. The result of this invocation is
then bound to the name of the function.
As an example, suppose we wanted to trace when a function is called by
printing out the name of the function as well as its arguments. We
could define a higher-order function that takes in a function and
returns a new nested function that first prints out the name of the
original function and its arguments and then calls it:
Here, we make use of variadic arguments to pass any number of
arguments to the original function. (For simplicity, we ignore keyword
arguments.) We can then use decorator syntax to apply this to a
function:
Notice that the recursive calls also call the transformed function.
This is because the name factorial is now bound to the nested
tracer function in the enclosing environment of factorial(), so
looking up the name results in the tracer function rather than the
original one. A side effect of this is that we have mutual recursion
where a set of functions indirectly make recursive calls through each
other. In this case, the tracer calls the original factorial(),
which calls the tracer, as shown in the diagram in
Figure 25 for factorial(2) from Python Tutor.
Nested function definitions allow the construction of functions at
runtime, fulfilling one of the requirements for functions to be a
first-class entity. So far, however, we’ve only seen nested function
definitions that are named, introducing a binding into the definition
environment. This is in contrast to other first-class entities, such
as data values, that can be created without being bound to a name.
Just like it can be useful to construct a value without a name, such
as when passing it as an argument or returning it, it can be useful to
construct unnamed functions. These are called anonymous or lambda
functions.
Lambda functions are ubiquitous in functional languages, but many
common imperative languages also provide some form of lambda
functions. The syntax and capabilities differ between different
languages, and we will examine a few representative examples.
Lambdas are a common construct in the Lisp family of languages, those
languages being primarily functional, and Scheme is no exception. The
lambda special form constructs an anonymous function:
(lambda(<parameters>)<body>)
A function definition using the define form can then be considered
a shorthand for a variable definition and a lambda:
Nested functions in Scheme use static scope, so the anonymous function
has access to the variable n in its definition environment. It
then adds its own argument x to n, returning the sum.
Scheme is not purely functional, allowing mutation of variables and
compound data. Nested functions, whether anonymous or not, can modify
variables in their non-local environment. The following function
creates a counter function that returns how many times it has been
called:
Python supports anonymous functions with the lambda expression.
This takes the following form:
lambda<parameters>:<bodyexpression>
The syntax of lambda expressions in Python produce a constraint on
anonymous functions that is not present in named nested functions: the
body must be a single expression, and the value of that expression is
automatically the return value of the function. In practice, this
limitation is usually not a problem, since lambdas are often used in
functional contexts where statements and side effects may not be
appropriate.
The following is a definition of the greater_than() higher-order
function that uses a lambda:
As can be seen in this example, simple nested functions that are used
in only a single place can be written more succinctly with a lambda
expression than with a definition statement.
While lambda functions in Python have access to their definition
environment, they are syntactically prevented from directly modifying
bindings in the non-local environment.
Java does not allow nested function definitions, but it does have
syntax for what it calls “lambda expressions.” In actuality, this
construct constructs an anonymous class with a method corresponding to
the given parameters and body, and the compiler infers the base type
of this class from the context of its use.
The following example uses a lambda expression to construct a
functor-like object:
IntPredicategt3=makeGreaterThan(3);System.out.println(gt3.test(2));// prints out falseSystem.out.println(gt3.test(20));// prints out true
Java allows a lambda to take in any number of arguments, and providing
types for the parameters is optional. The body can be a single
expression or a block containing arbitrary statements.
On the other hand, Java places a significant restriction on lambda
expressions. A lambda can only access variables in its definition
environment that are never reassigned, and it cannot modify them
itself. This is because lambdas are not implemented as closures, but
rather as functor-like objects that store “captured” variables as
members. The following is effectively equivalent to the code above,
but using named classes and methods:
Like Java, C++ has lambda expressions, but they provide more
functionality than those in Java. A programmer can specify which
variables in the definition environment are captured, and whether they
are captured by value or by reference. The former creates a copy of a
variable, while the latter allows a captured variable to be modified
by the lambda.
The simplest lambda expressions are those that do not capture anything
from the enclosing environment. Such a lambda can be written as a
top-level function instead [3], and C++ even allows a captureless
lambda to be converted to a function pointer. For example, the
following code passes a lambda function to a higher-order function
that takes in a function pointer:
The code constructs a lambda function that returns true if the first
element is bigger than the second, and passing that to
max_element() finds the minimum rather than the maximum element.
Lambdas that capture variables, whether by value or by reference, have
state that is associated with a specific evaluation of a lambda
expression, and this state can differ between different calls to the
enclosing function. As a result, such a lambda is not representable as
a top-level function. Instead, C++ implicitly defines a functor type
for a capturing lambda. Evaluating a capturing lambda expression
constructs an instance of this functor type, with the captured values
and references stored as non-static members. Since the functor type is
implicitly defined, type deduction with the auto keyword is
usually used where the type of the functor is required.
The following is an example that uses a lambda to define a greater-than
functor:
automake_greater_than(intthreshold){return[=](intvalue){returnvalue>threshold;};}intmain(){autogt3=make_greater_than(3);cout<<gt3(2)<<endl;// prints 0cout<<gt3(20)<<endl;// prints 1cout<<make_greater_than(30)(20)<<endl;// prints 0}
The = in the capture list for the lambda specifies that all
variables from the enclosing environment that are used by the lambda
should be captured by value. The code above is equivalent to the
following that explicitly uses a functor:
As indicated in the code above, a variable captured by value is
implicitly qualified as const.
An enclosing variable may also be captured by reference. However, a
variable that is captured by reference does not have its lifetime
extended. The reasoning for this is twofold. The first, practical
reason is that C++ implementations generally use stack-based
management of automatic variables, and when a function returns, its
activation record on the stack is reclaimed. Requiring that a variable
live past its function invocation prevents activation records from
being managed using a stack. The second, more fundamental reason is
that the RAII (i.e. scope-based resource management) paradigm in C++
requires that when an automatic variable goes out of scope, the
destructor for its corresponding object is run and the object
reclaimed. Relaxing this requirement would result in undesirable
effects similar to those of finalizers in garbage-collected languages.
The end result is that a lambda functor that captures by reference
should not be used past the existence of its enclosing function
invocation. The following counter definition is therefore erroneous:
The lifetime of the count variable ends when make_counter()
returns, so that calling the lambda functor afterwards erroneously
uses a dead object.
An alternative is to capture count by value, which stores a copy
as a member of the lambda, and then mark the lambda as mutable.
This removes the implicit const qualification from variables
captured by value, allowing them to be modified:
We now take a look at some common computational patterns in functional
programming. We will look at how to abstract these patterns as
higher-order functions, as well as how to use them with lambda
functions.
A number of functional patterns operate over sequences. These patterns
take in a sequence and a function and apply the function to elements
of the sequence, producing a new sequence or value as a result. Since
these are functional patterns, the original sequence is left
unchanged.
The map pattern takes a sequence and a function and produces a new
sequence that results from applying the function to each element of
the original sequence. For example, the following adds 1 to each
element of a Scheme list:
>(map(lambda(x)(+x1))'(123))(234)
We can define this higher-order function as follows, using the name
map1 to avoid conflict with the built-in Scheme map:
Applying map1 to an empty list results in an empty list. Otherwise,
map1 applies the given function to the first item in the list and
recursively calls map1 on the rest of the list.
The built-in Scheme map also works with a non-unary function, as
long as the number of lists provided matches the number of arguments
the function expects. For instance, we can apply cons to
corresponding elements from two lists as follows:
>(mapcons'(123)'(456))((1.4)(2.5)(3.6))
The argument lists must have the same length. We can define our own
version of this as a variadic procedure:
We use map1 to obtain the first element from each list, as well as
the rest of each list. We then use apply to apply func to the
first elements, as well as to recursively map func across the rest
of the lists.
Python has a similar built-in map() that takes in a function and
at least one sequence, producing a new sequence that results from
applying the function to corresponding elements from the input
sequences. Unlike Scheme’s version, Python allows the sequences to
differ in length, with the result sequence having the same length as
the shortest input sequence.
In the reduce pattern, a two-argument function is applied to the
first two items in a sequence, then it is applied to the result and
the next item, then to the result of that and the next item, and so
on. A reduction may be left or right associative, but the former is
more common. Figure 26 illustrates the difference
between left- and right-associative reductions.
Often, if only a single item is in the sequence, that
item is returned without applying the function. Some definitions allow
an initial value to be specified as well for the case in which the
sequence is empty.
The following examples compute the sum and maximum element of a Scheme
list:
The filter pattern uses a predicate function to filter items out of
a list. A predicate is a function that takes in a value and returns
true or false. In filter, elements that test true are retained while
those that test false are discarded.
The following example filters out the odd elements from a list:
The any pattern is a higher-order version of or (disjunction).
It takes a predicate and applies the predicate to each successive item
in a list, returning the first true result from the predicate. If no
item tests true, then false is returned. Some languages use the name
find for this pattern rather than any.
The following examples search a list for an even value:
Programs often compose functions, applying a function to the result of
applying another function to a value. Wrapping these two function
applications together in a single function enables both operations to
be done with a single call. For example, the following multiplies each
item in a list by three and then adds one:
Partial application allows us to specify some arguments to a function
at a different time than the remaining arguments. Supplying \(k\)
arguments to a function that takes in \(n\) arguments results in a
function that takes in \(n-k\) arguments.
As an example, suppose we want to define a function that computes
powers of two. In Python, we can supply 2 as the first argument to the
built-in pow() function to produce such a function. We need a
partial-application higher-order function such as the following:
Python actually provides a more general implementation of
partial() that works for keyword arguments as well in the
functools module. C++ provides partial application using the
bind() template in the <functional> header.
A related but distinct concept is currying, which transforms a
function that takes in \(n\) arguments to a sequence of \(n\)
functions that each take in a single argument. For example, the
pow() function would be transformed as follows:
>>> curried_pow(2)(3)8
The curried version of the function takes in a single argument,
returning another function. The latter takes in another argument and
produces the final value. Since the original pow() takes in two
arguments, the curried function chain has length two.
We can define currying for two-parameter functions as follows in
Python:
Then we can call curry2(pow) to produce a curried version of
pow().
We can also define an “uncurry” operation that takes in a function
that must be applied to a sequence of \(n\) arguments and produce
a single function with \(n\) parameters. The following does so for
a sequence of two arguments:
Some functional languages, such as Haskell, only permit functions with
a single parameter. Functions that are written to take in more than
one parameter are automatically curried.
An running program encompasses two types of state: the data that the
program is using and the control state of the program, such as the
stack of active functions and the code locations in each of those
functions. This control state can be represented in the form of a
continuation.
A continuation can be invoked in order to return control to a
previous state. Since a continuation only represents control state,
invoking a continuation does not return data to their previous
state. Instead, data retain the values they had at the time the
continuation was invoked. The following is an analogy of invoking a
continuation by Luke Palmer:
Say you’re in the kitchen in front of the refrigerator, thinking
about a sandwitch [sic]. You take a continuation right there and
stick it in your pocket. Then you get some turkey and bread out of
the refrigerator and make yourself a sandwitch, which is now sitting
on the counter. You invoke the continuation in your pocket, and you
find yourself standing in front of the refrigerator again, thinking
about a sandwitch. But fortunately, there’s a sandwitch on the
counter, and all the materials used to make it are gone. So you eat
it. :-)
In most non-functional languages, a continuation only exists in
implicit form, and there is a restricted set of operations that can be
done to invoke a continuation. In many functional languages, however,
continuations are first-class entities that can be passed as
parameters and returned from functions. We first examine restricted
forms of continuations before considering the more general,
first-class version.
Simple forms of control flow, such as conditionals and loops, do not
involve continuations, since they do not return to a previous state of
control. Subroutines and exceptions, on the other hand, do revert
control to a previous state and thus make implicit use of
continuations.
Subroutines involve transfer of control between a caller and callee.
When a subroutine is called, the control state of the caller must be
saved, so that when the subroutine completes, control can be
transferred back to the caller. Implementations make use of activation
records and call stacks that record the sequence of active calls as
well as information about how to return execution to a previous call.
These data structures represent the control state of a program and
thus constitute a continuation.
Languages restrict how the implicit continuation representing a
caller’s state can be invoked. In some languages, including many
functional languages such as Scheme, the caller’s continuation is
only invoked when the subroutine completes normally. Other languages
have a mechanism to terminate a subroutine early, sometimes called
abrupt termination, and invoke the continuation of the caller. In
imperative languages, this usually takes the form of a return
statement. For example, the following Python function uses a return
to immediately invoke the caller’s continuation:
deffoo(x):returnx# invoke caller's continuation# more code, but not executedifx<0:bar(x)baz(x)...
As with any continuation, invoking a caller’s continuation does not
restore the previous state of data. For example, consider the
following Python code:
When the call to inner() completes, the continuation of
outer() is resumed, but the value of x is not restored to its
state before the call to inner(). Instead, it retains its modified
value, and the code prints 1.
A more general concept provided by some languages is a coroutine,
which involves two routines passing control to each other by invoking
each other’s continuation. Coroutines differ from mutual recursion in
that each routine’s control state is resumed when it is invoked rather
than creating a fresh function invocation with its own state.
The following is pseudocode for coroutines that pass control to each
other, with one producing items and the other consuming them:
Both coroutines yield control to the other. Unlike with subroutines,
when a coroutine is passed control, execution resumes from where it
previously paused and in the context of the same environment.
Python provides an implementation of coroutines over a tasking layer,
with several abstractions for passing data between running coroutines
and waiting for completion of an action. The following implements the
producer/consumer model using an asyncio.Queue for passing values
between the producer and consumer:
importasyncioq=asyncio.Queue(2)# queue capacity of 2asyncdefproduce():foriinrange(5):print('[producer] putting',i)awaitq.put(i)print('[producer] done putting',i)asyncdefconsume():foriinrange(5):print('[consumer] got:',awaitq.get())loop=asyncio.get_event_loop()loop.run_until_complete(asyncio.gather(produce(),consume()))
The latter two statements start the producer and consumer coroutines
running and wait for their completion. The producer passes control to
the coroutine returned by q.put(i), which places an item into the
queue. Execution will not return to the producer until this completes,
so the producer will be forced to wait if the queue is full. The
consumer extracts items from the queue using the q.get()
coroutine, waiting if no items are available. The following is the
output when the code is run:
Exceptions also cause control to be passed from one execution state to
an earlier one, but unlike returning from a subroutine, the receiver
of control need not be the direct caller of a function. Upon entering
a try block, the control state is saved and the associated
exception handlers are added to a stack of active handlers. When an
exception is raised, the handler stack is searched for a handler that
can accommodate the exception type, the continuation of the associated
function is invoked, and the handler code is executed.
As a concrete example, consider the following Python code:
When the try statement in the invocation of foo(3) is reached.
the associated exception handler is added to the handler stack.
Execution proceeds to the call to bar(3) and then to baz(3),
which raises an exception. This passes control to the first exception
handler that can handle an exception of type Exception, which was
located in the call to foo(3). Thus, the latter’s continuation is
invoked and the exception handler is run.
The specific mechanisms used to provide exceptions vary between
languages and implementations. Some languages don’t incorporate
exceptions directly but provide a control mechanism that enables an
exception mechanism to be built on top of it. For example, the C
standard library header setjmp.h defines a setjmp() function
that saves the execution state of a function, and a corresponding
longjmp() function that restores the state at the time of the call
to setjmp(). Exceptions can also be implemented with first-class
continuations, as we will see below.
A generator is a generalization of a subroutine, allowing its
execution to be paused and later resumed. A subroutine is always
executed from its entry point, and every entry into a subroutine
creates a new activation record. On the other hand, a generator can
suspend its execution, and the programmer can resume execution of the
generator at the point where its execution state was suspended and
using the same activation record. Thus, the paused state of a
generator is a form of continuation.
Generators are usually used to write iterators that compute their
values lazily. When a generator computes an item, it yields the item
to its caller by invoking the continuation of the caller, much like a
subroutine. Upon resumption of the generator, the next value is
computed and yielded to its caller, which need not be the same
function as the previous caller.
The following is a generator in Python that produces an infinite
sequence of natural numbers:
defnaturals():num=0whileTrue:yieldnumnum+=1
Generators in Python implement the same interface as an iterator, so
the next item can be obtained by calling the next() function on a
generator:
The sequence of values produced by this generator is finite, and after
the last value is produced and the body of range2() exits, a
StopIteration exception is automatically raised:
>>> values=range2(0,10,3)>>> next(values)0>>> next(values)3>>> next(values)6>>> next(values)9>>> next(values)Traceback (most recent call last):
File "<stdin>", line 1, in <module>StopIteration
A StopIteration is used by the Python for loop to determine
the end of an iterator:
>>> foriinrange2(0,10,3):... print(i)...0369
As another example, we can define a unary version of the built-in
map using a generator:
As can be seen from this example, map stops when the shortest
input iterable is exhausted. We can attempt to write a variadic
generator similar to map:
We start by obtaining an iterator from each iterable, and then
constructing a list that will hold an element from each iterator,
initialized with dummy zero values. We follow this with an infinite
loop that obtains the next item from each iterator, storing it in the
list, invoking func on these items, and yielding the result. When
the shortest iterator is exhausted, invoking next() on it will
raise a StopIteration, and our intent was for this to end the
map_variadic() generator as well. Unfortunately, Python does not
allow a StopIteration to be propagated out of a generator:
>>> list(map_variadic(lambdax,y:x-y,[1,2,3],(-4,-5,-6,-7)))Traceback (most recent call last):
File "<stdin>", line 6, in map_variadicStopIterationThe above exception was the direct cause of the following exception:Traceback (most recent call last):
File "<stdin>", line 1, in <module>RuntimeError: generator raised StopIteration
Instead, a generator is required to terminate normally or with a
return when it is complete. Thus, we need to catch the
StopIteration and then exit:
Here, we’ve used a dummy pass statement in the except clause,
since execution will proceed to the end of the generator body and
exit. It would be equally valid to use a return statement instead.
The generator now works as intended:
The yieldfrom statement can be used to delegate to another
generator (or iterator). The following is a definition of a
positives() generator that delegates to an instance of
naturals():
>>> defpositives():... numbers=naturals()... next(numbers)# discard 0... yield fromnumbers# yield the remaining items in numbers...>>> numbers=positives()>>> next(numbers)1>>> next(numbers)2>>> next(numbers)3
We construct a naturals() generator, discard the initial 0, and
then use yieldfrom to produce the remaining items from the
naturals() generator.
Python also has generator expressions, similar to list comprehensions,
that succinctly produce a generator. The following produces a
generator of negative integers from naturals():
As with list comprehensions, the filtering conditional is optional in
a generator expression.
Generators are also called semicoroutines, since they involve a
standard routine that passes control to a resumable routine. Unlike a
coroutine, however, a generator can only return control to its caller,
while a full coroutine can pass control to any other coroutine.
In some languages, continuations are first-class entities, allowing
the current control state to be saved in an explicit data structure,
passed as a parameter, and invoked from arbitrary locations.
First-class continuations can be used to emulate any of the restricted
forms of continuations above. Depending on the language, it may only
be permitted to invoke a continuation once, or a continuation may be
resumed any number of times.
In Scheme, the call-with-current-continuation procedure, often
abbreviated as call/cc, creates a continuation object representing
the current control state. The call/cc procedure must be passed an
argument:
(call-with-current-continuation<procedure>)
Here, <procedure> must be a Scheme procedure that takes an
argument, and call/cc invokes this procedure with the newly
created continuation object as the argument. The called procedure may
use the continuation like any other data item, including discarding
it, saving it in a data structure, and returning it, as well as
invoking it. For example, in the following code, the procedure
discards the continuation and returns a value normally:
>(+1(call/cc(lambda(cc)3)))4
The continuation object constructed by the invocation of call/cc
above represents the following execution state:
(+1<value>)
Here, <value> replaces the call to call/cc, and it will be
replaced by the value with which the continuation is invoked.
If the procedure invoked by call/cc returns a value normally, the
invocation of call/cc evaluates to that same value, the same
behavior as a standard function call. In the example above, the
procedure returns the value 3, which replaces the call to call/cc,
resulting in a final value of 4.
On the other hand, if the continuation created by call/cc is
invoked, then control resumes at the location of the call/cc. A
continuation must be passed a value when it is invoked, and the
call/cc evaluates to that value:
>(+1(call/cc(lambda(cc)(cc5)3)))6
In the code above, the continuation represents the same execution
state of (+1<value>). The function argument of call/cc
invokes the continuation with value 5, causing execution to
immediately resume at the point where call/cc is called, with the
value 5 replacing the call to call/cc, as if it were a standard
function call that produced the given value. This results in the
execution (+15), resulting in a final value of 6.
More interesting behavior can occur when a continuation is saved in a
variable or data structure. Consider the following:
>(definevar(call/cc(lambda(cc)cc)))
The procedure called by call/cc returns the continuation, so the
call/cc invocation evaluates to the continuation, which is then
bound to var. The continuation itself represents the execution:
(definevar<value>)
We can bind another variable to the same object:
>(definecontvar)
Now we can use this new variable to invoke the continuation:
>(cont3); executes (define var 3)>var3>(cont4); executes (define var 4)>var4
Invoking the continuation with a value causes evaluation to resume at
the call/cc, with the given value replacing the call/cc. Thus,
invoking cont with the value 3 results in the following:
The base case is a call to call/cc. Then when (factorial3) is
called, the execution state when the base case is reached is:
(*3(*2(*1<value>)))
As before, <value> represents the call to call/cc. The
argument to call/cc sets the global variable cont to refer to
the newly created continuation and then evaluates normally to 1. The
value 1 thus replaces the call/cc, resulting in a final value of
6:
>(factorial3)6
If we then invoke the continuation with the value 3, the 3 replaces
the call/cc in the execution state represented by the
continuation:
>(cont3); executes (* 3 (* 2 (* 1 3)))18
If we call (factorial5), cont is modified to refer to a
continuation representing the execution:
(*5(*4(*3(*2(*1<value>)))))
Invoking the continuation on 4 then results in 480:
We can use first-class continuations to implement a basic mechanism
for aborting a computation and signaling an error. We begin with a
simple procedure to print an error message:
This procedure expects to be called with a message string, and it
prints out Error: followed by the message to standard out.
However, invoking the procedure does not abort the computation in the
caller. Thus, if we encounter an error in a larger computation,
invoking report-error causes a message to print but continues
where the computation left off. The following is an example:
(define(inversex)(if(=x0)(report-error"0 has no inverse")(/1x)))
The inverse procedure reports an error if the argument x is
zero. However, it still returns the (undefined) result of calling
report-error to the caller of inverse. This can result in an
error at the interpreter level:
> (+ (inverse 0) 1)
Error: 0 has no inverse
+: contract violation
expected: number?
given: #<void>
argument position: 1st
other arguments...:
1
context...:
[context elided]
In this Scheme implementation, the newline procedure returns a
special #void value, which gets returned by report-error and
then by inverse. The caller of inverse then attempts to add 1
to this result, resulting in an interpreter error.
In order to abort the computation entirely once an error has been
signaled, we can make use of a continuation. We arrange for the
continuation to save the control state at the top level of a program.
but with a following invocation to report-error if an error
message is provided:
Here, the call to call/cc saves the control state with the program
about to bind the name message within a let to the result of
invoking the continuation. In the initial computation, the
continuation object is passed to the lambda, which immediately
returns it. The call to call/cc evaluates to this value, so
message is bound to the continuation object itself, and the body
of the let is evaluated. This checks if message is a string,
calling report-error if this is the case. The let as a whole
evaluates to the value of message, which is then bound to
error-continuation in the global frame.
If we invoke error-continuation again, execution will resume at
the point of binding message, and it will eventually result in
error-continuation being rebound to something other than the
continuation object. To avoid losing the continuation, we can bind
another name to it:
(defineerrorerror-continuation)
Now even if error-continuation is rebound, the name error
still refers to the continuation object.
If we invoke error with a string, the continuation is invoked with
that value, and the value is plugged into where the continuation was
created. Thus, message is bound to the string, and the body of the
let is evaluated. Since message is a string, report-error
is called, printing an error message. The let evaluates to the message
string, which is then bound to the name error-continuation in the
global frame. At this point, execution has reached the top level, so
computation is completed without causing an error in the interpreter.
If we repeat our previous example, but invoking error rather than
report-error, we get the following:
(define(inversex)(if(=x0)(error"0 has no inverse")(/1x)))>(+(inverse0)1)Error:0hasnoinverse
We no longer have an error reported by the interpreter itself.
First-class continuations can be used to emulate the more restricted
control constructs provided by imperative languages. For instance,
Scheme does not provide a specific mechanism that allows a procedure
to terminate abruptly, returning a value to the caller. However, we
can emulate call and return, including abrupt returns, with
continuations. We do so by explicitly representing the call stack in a
data structure that provides push and pop operations:
We will use this call stack to store a procedure’s continuation when
it calls another procedure. A return just pops a continuation off the
stack and invokes it with the given return value:
(define(returnvalue)((pop-call)value))
We then provide a mechanism for saving a caller’s continuation, by
pushing it onto the call stack, and invoking the callee. For
simplicity, we restrict ourselves to single-argument functions here,
but this can be generalized using Scheme’s variadic arguments.
We can then write procedures that use the call stack to terminate
abruptly:
(define(foox)(if(<=x10)(returnx); return x if <= 10)(let((y(-x10)))(return(+x(/xy))); otherwise return x + x / (x - 10))(somemorestuffhere); control never reaches here)(define(barx)(return(-(callfoox))); call foo and return the negation(deadcode); control never reaches here)