Animated Problem Solving An Introduction To Program Design Using
Animated Problem Solving An Introduction To Program Design Using
Marco T. Morazán
Animated
Problem Solving
An Introduction to Program Design
Using Video Game Development
Texts in Computer Science
Series Editors
David Gries, Department of Computer Science, Cornell University, Ithaca, NY, USA
Orit Hazzan , Faculty of Education in Technology and Science, Technion—Israel
Institute of Technology, Haifa, Israel
Titles in this series now included in the Thomson Reuters Book Citation Index!
’Texts in Computer Science’ (TCS) delivers high-quality instructional content for
undergraduates and graduates in all areas of computing and information science,
with a strong emphasis on core foundational and theoretical material but inclusive
of some prominent applications-related content. TCS books should be reasonably
self-contained and aim to provide students with modern and clear accounts of topics
ranging across the computing curriculum. As a result, the books are ideal for semester
courses or for individual self-study in cases where people need to expand their
knowledge. All texts are authored by established experts in their fields, reviewed
internally and by the series editors, and provide numerous examples, problems, and
other pedagogical tools; many contain fully worked solutions.
The TCS series is comprised of high-quality, self-contained books that have broad
and comprehensive coverage and are generally in hardback format and sometimes
contain color. For undergraduate textbooks that are likely to be more brief and
modular in their approach, require only black and white, and are under 275 pages,
Springer offers the flexibly designed Undergraduate Topics in Computer Science
series, to which we refer potential authors.
© The Editor(s) (if applicable) and The Author(s), under exclusive license to Springer Nature Switzerland
AG 2022
This work is subject to copyright. All rights are solely and exclusively licensed by the Publisher, whether
the whole or part of the material is concerned, specifically the rights of translation, reprinting, reuse
of illustrations, recitation, broadcasting, reproduction on microfilms or in any other physical way, and
transmission or information storage and retrieval, electronic adaptation, computer software, or by similar
or dissimilar methodology now known or hereafter developed.
The use of general descriptive names, registered names, trademarks, service marks, etc. in this publication
does not imply, even in the absence of a specific statement, that such names are exempt from the relevant
protective laws and regulations and therefore free for general use.
The publisher, the authors, and the editors are safe to assume that the advice and information in this book
are believed to be true and accurate at the date of publication. Neither the publisher nor the authors or
the editors give a warranty, expressed or implied, with respect to the material contained herein or for any
errors or omissions that may have been made. The publisher remains neutral with regard to jurisdictional
claims in published maps and institutional affiliations.
This Springer imprint is published by the registered company Springer Nature Switzerland AG.
The registered company address is: Gewerbestrasse 11, 6330 Cham, Switzerland
To my parents, Doris and Marco, who taught
me to love teaching and to realize that having
an education is not a privilege but a
responsibility.
Preface
than just code written using a programming language. Remember that a program is
a solution to a problem. Therefore, a program has a design, code, examples of how it
works, and tests. That is, it communicates how the problem is solved and illustrates
that the solution works. If any of the mentioned components are missing, then we
have an incomplete program. Would you believe someone who simply told you that
n2 , where n is a nonnegative integer, is the sum of the first n odd numbers? Many
readers would be skeptical. What if they also provided the following examples:
02 = 0
22 = 1 + 3
42 = 1 + 3 + 5 + 7
It is very likely that most readers would now feel more confident that the claim is
true. It is the same in programming. We cannot simply say that here is a function
that does this or that. We need to explain how the function computes its value, and
we need to have examples that show how it works. The steps taken to design a
program in a systematic manner is called a design recipe. In this textbook, you shall
study many different design recipes. Each design recipe shall become a tool in your
problem-solving toolbox.
There are two problem-solving techniques that are emphasized throughout the
book: divide and conquer and iterative refinement. Divide and conquer is the process
by which a large problem is broken into two or more smaller problems that are easier
to solve and then the solutions for the smaller pieces are combined to create an answer
to the problem. Iterative refinement is the process by which a solution to a problem
is gradually made better—like the drafts of an essay. Mastering these techniques is
essential to becoming a good problem solver and programmer.
Finally, problem solving ought to be fun. To this end, this book promises that by
the end of it you will have designed and implemented a multiplayer video game that
you can play with your friends over the internet. To achieve this, however, there is a
lot about problem solving and programming that you must first learn. The game is
developed using iterative refinement. As we learn about programming, we shall apply
our new knowledge to develop increasingly better versions of the video game. In
fact, every skill you develop for problem solving and program design is transferable
to other (non-programming) domains and to other programming languages.
The book uses the Racket student languages to write programs. These languages
are chosen for several reasons. The first is that they have an error-messaging system
specifically designed for beginners. This means that unlike common programming
languages the error messages are likely to make sense to beginners. If you do not
understand an error message, do not hesitate to ask your professor or search for
help online. The second is that the syntax is simple and easy to understand. This
is important because the emphasis is always on problem solving and not on how
1 The Languages and the Parts of the Book ix
to correctly write expressions. The third is that the student languages progressively
become richer. At the beginning, you have fewer features at your disposal and,
therefore, the possible errors are fewer. The fourth reason is that the student languages
come with powerful libraries to create graphics, animations, and video games. These
libraries allow students to inject their own personalities in the development of games
and animations. You are strongly encouraged to be creative. Finally, the fifth reason
is that the Racket student languages are likely to put all students on the same playing
field. Most students will be learning the syntax of the programming language together
for the first time.
The book is divided into five parts. Part I focuses on the basics. It starts with
how to write expressions. Once expressions are mastered, the first abstraction lesson
introduces us to functions. In addition, this part introduces you to conditional expres-
sions that allow you to write programs that make decisions. Just this much knowledge
allows us to write interactive programs and puts us on our way to a multiplayer video
game. As you shall discover, decision-making is fundamental to solving problems
that involve information that has many varieties. For example, the whole numbers
may be positive or negative—two varieties—and how a whole number is processed
depends on which variety a given number belongs to. Think about how to compute
the absolute value of a whole number.
Part II introduces you to compound data of finite size. Compound data has multiple
values associated. For example, a point on the Cartesian plane is compound data of
finite size. There are two values: an x coordinate and a y coordinate. Being able to
define compound data of finite size to represent elements in the real or an imaginary
world is a powerful skill to develop.
Part III introduces you to compound data of arbitrary size. This is data that has
multiple values, but the number of values is not fixed. Once again, think about a
grocery list. Sometimes there are no items in the list and at other times there may
be 10, 6, or 17 items in the list. This is where you are introduced to structural
recursion—a powerful data-processing strategy that uses divide and conquer to
process data whose size is not fixed. The types of data that are introduced are lists,
intervals, natural numbers, and binary trees. The knowledge developed is used to
develop a video game that is more challenging for the player.
Part IV delves into abstraction. This section is where we learn how to eliminate
repetitions in our solutions to problems. In fact, we learn how different data can
be processed and different problems can be solved in exactly the same way. You
are introduced to generic programming, which is abstraction over the type of
data processed. This leads to the realization that functions are data and, perhaps
more surprising, that data are functions. In other words, the line between data and
functions is artificial—a fact that is not emphasized enough in Computer Science
textbooks. This realization naturally leads to object-oriented programming—a topic
that you are likely to study extensively.
Part V introduces you to distributed programming—using multiple computers to
solve a problem. This is a topic that until now has never been addressed in a textbook
for beginning programmers. The fact that you develop proficiency in program design
makes it possible for this topic, common in modern computer applications, to be
x Preface
discussed. If you have ever sent a text message or have ever played a game online,
then you have benefitted from and have used a distributed program. It is impossible,
of course, to discuss all the nuances of distributed programming in this textbook.
Nonetheless, you are introduced to a modern trend that is likely to be common
throughout your professional career and beyond.
2 Acknowledgments
This book is the product of over ten years of work at Seton Hall University build-
ing on the shoulders of giants in Computer Science. There are many persons and
groups who deserve credit for informing my work. The Racket community has been
unequivocal in its support for the techniques that I have developed. There is an un-
payable debt of gratitude owed to Matthias Felleisen from Northeastern University
for our discussions over the years about Computer Science education, Liberal Arts
education, and program design. My students and I have greatly benefitted from his
support. Other Racketeers who have deeply influenced me are Shriram Krishna-
murthi, Matthew Flatt, Robert Bruce Findler, and Kathi Fisler. This textbook is a
tribute to our debates and their published work.
I would also like to thank the Trends in Functional Programming (TFP) and
the Trends in Functional Programming in Education (TFPIE) communities. These
communities provided (and continue to provide) a venue to discuss and present
work advancing Computer Science education. I am grateful to many individuals
including Peter Achten, Jurriaan Hage, Pieter Koopman, Simon Thompson, and
Marko van Eekelen. Their insightful feedback has informed much of the material in
this textbook.
Finally, I would like to thank Seton Hall University and its Department of Com-
puter Science for supporting the development of the work presented in this textbook.
In particular, the support of John T. Saccoman, Manfred Minimair, and Daniel Gross
is appreciated. Most of all, I am grateful to all my CS1 students over the past decade
who have informed my Computer Science education efforts. It is likely true that my
students have learned a great deal in my courses, but it is an absolute certainty that
I have learned more from them. They have refined the delivery of every idea found
in this textbook. I am especially grateful to all my undergraduate tutors and teach-
ing assistants, including Shamil Dzhatdoyev, Josie Des Rosiers, Nicholas Olson,
Nicholas Nelson, Lindsey Reams, Craig Pelling, Barbara Mucha, Joshua Schappel,
Sachin Mahashabde, Rositsa Abrasheva, Isabella Felix, and Sena Karsavran. Without
my dedicated students at Seton Hall University and their insight into what students
understood, this textbook would have been impossible.
Contents
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii
1 The Languages and the Parts of the Book . . . . . . . . . . . . . . . . . . . . . viii
2 Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . x
xi
xii Contents
7 Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
38 The posn Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
39 Going Beyond the Design Recipe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
40 Revisiting in-Q1? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
41 What Have We Learned in This Chapter? . . . . . . . . . . . . . . . . . . . . . 166
12 Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
65 Creating and Accessing Lists in ISL+ . . . . . . . . . . . . . . . . . . . . . . . . 266
66 Shorthand for Building Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
67 Recursive Data Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
68 Generic Data Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
69 Function Templates for Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
70 Designing List-Processing Functions . . . . . . . . . . . . . . . . . . . . . . . . . 277
71 What Have We Learned in This Chapter? . . . . . . . . . . . . . . . . . . . . . 279
Part IV Abstraction
21 Encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
119 Local-Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
120 Lexical Scoping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
121 Using Local-Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483
121.1 Encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483
121.2 Readability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 488
121.3 Furthering Functional Abstraction . . . . . . . . . . . . . . . . . . . . 490
121.4 One-Time Expression Evaluation . . . . . . . . . . . . . . . . . . . . 492
122 What Have We Learned in This Chapter? . . . . . . . . . . . . . . . . . . . . . 497
Part VI Epilogue
We all solve problems every day. Have you ever thought about how you go about
problem solving? Do you randomly go about trying potential solutions to a problem
or do you think about how to solve the problem? Most of the time you probably think
about the problem to find a solution. A natural question that arises is how do we think
about a problem to find a solution. In other words, what steps do we take to arrive to
a plausible solution? The solution is plausible until we test it and feel confident that
it works. If it does not work, of course, we go back to thinking about the problem to
obtain a refined solution. Understanding how to think about problems and solutions
is where Computer Science and programming are beneficial to everyone.
Computer Science is not the study of computers just like Chemistry is not the
study of test tubes nor Astronomy is the study of telescopes. So, why is it called
Computer Science? Although there is no clear answer to this question, the best
guess is that Computer Science is an umbrella term for many disciplines whose
primary tool is the computer, such as programming language theory, algorithmics,
software engineering, data mining, robotics, and artificial intelligence. If Computer
Science is not the study of the computer, then what is Computer Science and what do
computer scientists do? Computer scientists solve problems. Unlike biologists who
solve problems in Biology or diplomats who solve problems in Diplomacy, computer
scientists solve problems that are relevant to all fields of study and to all facets of
human life. Stated simply, Computer Science is multidisciplinary. Therefore, the
best way to describe Computer Science is to say that it is the science of problem
solving. Programming is how computer scientists express solutions to problems.
Although Computer Science is a relatively young discipline,1 it has developed many
effective techniques to solve problems. This is why everyone ought to learn to design
programs. Problem solving is an essential skill just like reading, writing, and doing
arithmetic. Your journey through this book will help you learn effective problem
solving techniques.
1
The first Computer Science Program was established in 1953 at the University of Cambridge in
the United Kingdom. The first Computer Science Department in the United States was established
at Purdue University in 1962.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 3
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_1
4 1 The Science of Problem Solving
Two of the best-known problem solving techniques are divide and conquer and
iterative refinement. Problem solvers use divide and conquer when a larger problem
is decomposable into smaller problems. Instead of solving an entire problem in one
huge step, the problem is divided into a set of smaller problems. These subproblems
are all solved independently, and then their solutions are combined to formulate the
answer to the large problem. For example, consider computing your quiz average.
You can divide this problem into two smaller problems: sum the quiz grades and
count the number of quizzes. Once these subproblems are solved, the results are
combined by dividing the former by the latter to formulate the quiz average.
Iterative refinement is used to develop a solution in steps. This is particularly
useful to manage the complexity of a problem. Instead of developing a full answer
at once, you solve a simpler version of the problem. Once that is done, you add
complexity to the problem and re-solve it. This process continues until you have
a full answer. For instance, consider the problem of developing a video game like
Pacman. You may first create a game that only has Pacman. Then you create a game
that has Pacman and food. The next version of the game has Pacman, the food, and
one ghost. Finally, the last version of the game has Pacman, the food, and multiple
ghosts.
You have been taught to solve problems most of your life. This means that you have
been taught to compute. For example, you have been taught to compute meaning from
English expressions. You know that Thelma is that pig has a different meaning from
That is Thelma’s pig. Other languages that you have been trained to do computations
with are Arithmetic and Algebra. For example, you know how to compute the value of
the following expression: 5 * (8 + 2). In essence, we use expressions to describe
computations and we evaluate expressions to derive meaning. You may have never
thought explicitly about divide and conquer or about iterative refinement, but you
have been taught to use these techniques. Consider writing an essay. The first step
is to create an outline. This is where you decompose your argument into different
points. Perhaps, every point is implemented as a different section. You combine
the different sections into an essay by making sure that your argument easily flows
from one section to the next. As you can observe, you are using divide and conquer.
Staying with essay development, you first write a rough draft and then repeatedly
make improvements until you are happy with the result. In other words, you are using
iterative refinement.
If we are going to express solutions to problems using a computer, we need to use
a programming language much like we use English to communicate. A programming
language allows us to communicate to the computer what we want it to do. There are
many programming languages. Each has its strengths and weaknesses. You will learn
many programming languages as you explore the world of Computer Science. We
will start with a programming language called Beginning Student Language (BSL).
BSL is a programming language specifically designed to teach beginners like yourself
how to design and implement solutions to problems. A solution to a problem written
in BSL (or any other programming language) is called a program. When a program
is evaluated, we obtain its meaning. That is, we obtain the solution to an instance of
a problem.
3 Getting Started 5
3 Getting Started
2
A programming development environment is commonly called an integrated development envi-
ronment (IDE).
6 1 The Science of Problem Solving
definitions area, you may click the RUN button to evaluate your program. Let us try
this out. Type the following string3 in the definitions area:
"Hello World! I am DrRacket."
Now click on RUN. In the interactions window you will see the string printed.
Congratulations! You just wrote your first program. When you clicked on the RUN
button, you told DrRacket to evaluate your program. The value of a string is just
the string itself. Once DrRacket knows the value of a program, it is printed in the
interactions area.
** Ex. 3 — Write and run a program that prints your name and your age.
3
A string is anything inside,"", double quotes.
3 Getting Started 7
To do so, we need to know how to write valid expressions. That is, we need a
mechanism to describe how to write expressions. We will use a grammar to describe
expressions. A grammar consists of a series of production rules. A production rule
tells a programmer, for example, how to type valid expressions.
Figure 3 displays our initial grammar for expressions. Each production rule con-
sists of three parts. The leftmost part is the syntactic category being defined. If this
is missing, it means that it is the same syntactic category as the production rule
above it. The middle part is always the symbol ::=. The symbol ::= may be read
as is or as may be substituted by. The rightmost part is the definition of the syntactic
category. The first two rules state that an expression, expr, is either a string or a
number. The next rule states that a string starts and ends with ". In between, you
may have 0 or more characters. The * means 0 or more.4 Observe that character
is in a different font and surrounded by < and >. This indicates that it is a syntactic
category we only describe verbally. You may think of a character as anything that is
produced by a keystroke such as letters, spaces, and punctuation marks.5 The next
rule states that a number is either a positive or negative. A positive is a mag
(magnitude) and a negative is a - followed by a mag.
There are three rules for mag. These state that a mag may be an integer, a real, or
a fraction. An int is 1 or more digits (the + means 1 or more and is called Kleene
plus). A digit is an integer in [0..9]. A real is 0 or more digits followed by a .
followed by 1 or more digits. Finally, a fraction is an int followed by a followed
by a nonzero integer. In the grammar, the | means or. Therefore, a nonzero-int is
any digit from 1 to 9 followed by an integer.
4
In a grammar, a * is called Kleene star, named after the American mathematician Stephen Cole
Kleene.
5
Characters inside a computer are represented by an ASCII code. For example, the ASCII code for
a is 97. You will learn more about ASCII codes in a Computer Architecture course.
8 1 The Science of Problem Solving
We are now ready for our initial grammatical definition of a program. The last
rule in Fig. 3 states that a program is 0 or more expressions. Why do we need 0
or more expressions? Given that a program may compute an arbitrary number of
values, a program may have 0 or more expressions.
Let us try to write a new program. To write a new program, open a new tab in
DrRacket by using Ctrl-T. Ctrl-T means pressing the Ctrl and the T keys at the
same time. Now write a program in the definitions area to compute the name of this
textbook, the number one million, and negative 8.7 as follows:
"Program By Design"
1000000
-8.7
The result in the interactions area after clicking RUN is
"Program by Design"
1000000
-8.7
* Ex. 4 — Write and run a program that prints your year of birth and your age.
** Ex. 5 — Write and run a program that prints your favorite color, your lucky
number, and the largest prime number less than 25.
So far, all our programs have computed constant values, that is, values that we wrote
before running the program. In order for programs to be truly useful, we need to
be able to compute new values. That is, we need to be able to combine values to
create new values. Fortunately, BSL provides us with application expressions. An
application expression applies a function to one or more arguments. The function,
in essence, combines its inputs to create a new value and returns this new value.
Therefore, an application expression evaluates to the value returned by the function.
We extend the expr’s production rules in Fig. 3 to include application expressions
as follows:
expr ::= (<function> expr+ )
This production rule states that an application expression starts with an opening
parenthesis, followed by a function (the operator), followed by 1 or more expressions
(the operands), followed by a closing parenthesis. The function and each of its
arguments must be separated from each other by one or more spaces. BSL provides
us with many functions, for example, to combine numbers and to combine strings.
Table 1 lists some of the functions BSL provides along with sample uses.
At this point, you may feel that the syntax notation is a bit awkward. In your
Mathematics textbooks, for example, the basic operations (i.e., +, -, *, and /) are
4 Computing New Values 9
written using infix notation. This means that the operator is written in the middle
of the operands. For example, the sum of 1, 2, 3, 4, and 5 is written as 1 + 2 +
3 + 4 + 5. A function application, on the other hand, is written operator first and
then in parentheses the operands. For example, applying f to 3 is written as f(3).
In contrast, BSL uses prefix notation. That is, inside parentheses we first write the
operator and then the operands. Observe that the basic operations are functions, and
therefore, BSL treats them no differently than other functions (i.e., they are written
using prefix notation). Thus, the sum of 1, 2, 3, 4, and 5 is written as (+ 1 2 3 4
5) and applying f to 3 is written as (f 3). Always remember that when you want
to apply a function to some arguments, you must put in parentheses the function
first and then the arguments. As you can see, the translation from the syntax used
in Mathematics textbooks to BSL syntax is straightforward. One of the advantages
of BSL syntax is that it may be less typing as is the case for the sum of 1, 2, 3, 4,
and 5. More importantly, however, is that ambiguity cannot arise. For example, in
Mathematics syntax what is the value of 10 + 4 * 5? It depends if you add or you
multiply first. You probably remember operator precedence and believe the correct
value is 30. In BSL, we do not rely on remembering operator precedence. Prefix
notation and the use of parentheses always make it clear what arguments a function
is applied to. If the value of 10 + 4 * 5 is 30, then we write (+ 10 (* 4 5)) in
BSL. Observe that BSL syntax clearly communicates to the readers of our code that
the arguments to * are 4 and 5 and that the arguments to + are 10 and the value of
(* 4 5).
This last example explicitly shows us that application expressions may be nested.
In (+ 10 (* 4 5)), (* 4 5) is nested. There is no limit to the number or the depth
of nested expressions as subtly suggested by the above production rule for application
expressions. Always keep in mind that in an application expression for every opening
parenthesis, there must be a matching closing parenthesis. Furthermore, remember
that a closing parenthesis cannot simply be written anywhere in an application
expression. A closing parenthesis must appear after the last argument to a function.
Otherwise, the meaning of the application expression (i.e., what it evaluates to)
changes. For example, the following two expressions have different meanings:
(* (+ 4 1) 7 (- 15 5)) (* (+ 4 1 7) (- 15 5))
10 1 The Science of Problem Solving
In the first, the last argument to + is 1 and 7 is an argument for *. It evaluates to 25.
In the second, the last argument to + is 7 and 7 is not an argument for *. It evaluates
to 120. As you can see, the meaning of the above expressions is different. Always be
careful about where you close your parentheses.
There is one more thing you may be wondering. Do all functions take as input an
arbitrary number of expressions like + and *? The answer to this is no. It depends
on the function that is being applied. For example, string-length only takes one
input. If you are not sure if a function accepts an arbitrary number of arguments,
you have two options. You can try using it in a program or in the interactions area.
Consider string-append from Table 1. Can we give it more than two arguments?
Let us try it by typing after the prompt, >, in the interactions area the following:
(string-append "Hello" " " "World" "!" " " "I am DrRacket.")
After hitting Enter, the following value is printed:
"Hello World! I am DrRacket."
The answer to our question is that we can give string-append more than two
arguments. Below the output string, you get the DrRacket prompt again. DrRacket
is ready to evaluate another expression for you.
The second option is to consult DrRacket’s Help Desk as illustrated in Fig. 4.
A browser pops up and you can type BSL in the search box. After clicking on BSL,
5 Definitions and Interactions Areas Differences 11
you are taken to the documentation page for the BSL language. On this page a search
for string-append yields
(string-append s t z ...) → string
s : string
t : string
z : string
> (string-append "hello" " " "world" " " "good bye")
"hello world good bye"
The . . . means that the function takes as input an arbitrary number of arguments.
The arrow means returns. After the arrow, we find the type of value returned. Further
down, we see the type of each input: all are string type. The types of the inputs and
the type of the output define the signature of the function. It clearly informs anyone
who wishes to use string-append that if they provide strings as input, the function
returns a string as its value. In terms of high school Mathematics, you may think of
the input types as the domain of the function and the return type is the range of the
function. Finally, at the bottom we see a brief description of the function’s purpose
and an example of its use. Whenever you are unsure if BSL has a function or you are
unsure of how to use a BSL function, you may look for it in DrRacket’s Help Desk
to determine if it exists and, if so, the signature tells you the expected inputs and the
expected output.
As you have seen, expressions written in both the definitions and the interactions
areas can be evaluated. In the definitions area you need to click RUN. In the interactions
area you need to hit Enter. So, why are there two areas?
As stated before, the two areas are used for different purposes. In the definitions
area, we write programs. These are solutions to problems that we may save and
run multiple times. In the interactions area, we ask DrRacket to evaluate one-time
expressions, that is, expressions that we are not interested in running multiple times.
The difference may seem subtle, but as you advance through the first few chapters
of this book, the differences will become better delineated.
A good rule of thumb to follow is that if you wish to save the expressions you
write to solve a problem, then you write them in the definitions area. If you only
want to quickly have DrRacket evaluate an expressions for you, then write it in the
interactions window.
12 1 The Science of Problem Solving
It is an unfortunate fact that computers may crash. When this happens your unsaved
work is likely to be lost. Therefore, it is important that you regularly save your work.
It is a good idea for you to create a directory for the programs you will develop as
you read this book. You may even want to create subdirectories for each chapter.
This is an effective way to keep your files organized.
To save your work go the File menu. If it is the first time saving your cur-
rent program, click on Save Definitions As... as illustrated in Fig. 5. After
this, find the directory you wish to save your work in and enter a filename. If you
choose the filename Exercise-2-Chapter-4, your work is saved in a file named
Exercise-2-Chapter-4.rkt. The .rkt extension is automatically added to indi-
cate that it is a Racket file. In reality, it is a BSL file, but it is saved as a Racket
file given that BSL is a subset of Racket. If it is not the first time saving the current
program, click on Save Definitions inside the File menu. Alternatively, you
may simply use Ctrl+S.
7 Error Messages
Errors are part of the problem solving process and even the most experienced problem
solvers write programs with errors in them. Do not panic when you get an error
message. Remember the famous saying errāre hūmānum est (to err is human6). The
error message is DrRacket’s attempt to help you diagnose the error.
6
The complete saying, coined by Alexander Pope in his Essay on Criticism, is to err is human; to
forgive, divine.
7 Error Messages 13
An important lesson to absorb is that error messages help us diagnose and correct
bugs7 in our programs. An error message itself does not diagnose the bug nor does it
tell us how to fix the bug. That is left to us as program developers, because only the
program developers really know the intended meaning of expressions in a program.
DrRacket approximates where an error occurs, but the programmer must diagnose
and remedy the bug.
As you now know, essays and programs are both written following the production
rules of a grammar. The English grammar tells you how to write a valid English
expression. Similarly, the BSL grammar tells you how to write valid BSL programs.
When you write a valid BSL program and click on RUN, DrRacket evaluates your
program and prints the answer.
If you fail to write a valid BSL program and click on RUN, DrRacket prints an
error message. When DrRacket detects a grammatical error, an informative error
message is printed in the interactions window. The message details the grammatical
error. For example, type the following in the definitions area:
"Hello World! I am DrRacket.
After clicking RUN, the following error message appears in the interactions window:
read-syntax: expected a closing '"'
The error message states that it was expecting to find a closing double quote. As you
have probably already observed, the opening double quote does not have a matching
closing double quote. Always make sure to read error messages, because they are
usually helpful in determining the cause of errors.
Grammatical errors may also occur writing application expressions. Type the
following program:
(sting-append "I’m done" " " "and going home!")
After clicking RUN, the following error message is displayed:
sting-append: this function is not defined
DrRacket is telling us that it does not know the function sting-append. We can
clearly see that the name of the function is misspelled and we correct it as follows:
(string-append "I’m done" " " "and going home!")
7
The term computer bug stems from a moth found trapped in a computer relay in 1947. The bug was
removed and taped to the log book. The log book along with the moth are part of the Smithsonian
National Museum of American History collection in Washington D.C.
14 1 The Science of Problem Solving
Another kind of error that we may make is called a type error. Type errors mostly
occur in two ways. The first is when the wrong number of arguments are provided
to a function. Let us revisit a program from the previous subsection.
(+ (* 4 5 (/ 50 10)))
After clicking RUN, the following error message is displayed:
+: expects at least 2 arguments, but found only 1
DrRacket is telling us that it believes that we are not correctly using +. We are not
providing the right number of arguments to +. This is a very common mistake done
by beginners and it is important that we keep this in mind as we implement solutions
to problems.
The second is when the wrong type of argument is given to a function. For
example, type the following after the prompt in the interactions area:
(string-append "My lucky number is: " 8)
After clicking RUN, the following error message is displayed:
string-append: expects a string as 2nd argument, given 8
The string-append function expects all its inputs to be strings. In this example,
the second argument is a number. To fix this bug, we must make the second argument
a string as follows:
(string-append "My lucky number is: " "8")
After clicking RUN, the string is printed in the interactions area. It is worth highlight-
ing that "8" is not the same as 8. The former is a string and the latter is a number,
that is, they are different types of data.
Providing a function with the wrong type of input is a very common mistake.
Therefore, type theory is an important and growing discipline within Computer
Science. As we progress through this textbook, we will learn design techniques to
help us avoid type errors. Furthermore, we will learn how to exploit the type of data
being processed to design solutions (and programs) to problems.
You need to be aware of a third kind of error called a runtime error. Unlike gram-
matical and type errors, runtime errors are not detected until after an evaluation has
started. For example, type the following program in the definitions area:
(* 5 0)
(+ 5 0)
(/ 5 0)
(- 5 0)
The results after clicking RUN are displayed in Fig. 8. We can observe that the first
two expressions are evaluated and their results are printed. The third expression is
highlighted in pink signalling that an error has been detected during its evaluation.
The error message in the interactions window informs us that there has been an
attempt to divide by 0. The fourth expression is highlighted in black signalling that
it has not been evaluated.
Our programs may also contain runtime errors that do not generate an error
message. For example, write the following program to compute f(4), where f(x)
= 2x2 + 10x + 3:8
(+ (* 2 (sqr 4)) (* 10 4) 4)
After clicking RUN, no error message is generated and 76 is printed in the interactions
window. Observe, however, that 76 is not the value of f(4). These are probably the
hardest errors to fix, because no error message is generated. In this example, the bug
is rather easy to spot. The last 4 in the expression is a typo and should be 3.
8
sqr is a BSL function that squares its input. Look it up in the Help Desk!
18 1 The Science of Problem Solving
To help protect ourselves against this type of error, most programming languages,
including BSL, allow programmers to write unit tests. A unit test validates that two
different expressions evaluate to the same value. In order to write unit tests, we need
to expand our grammar as follows:
program ::= {expr | test}∗
test ::= (check-expect expr expr)
Now, a program is 0 or more expressions or tests. The curly braces are used to
delimit the scope of the Kleene star and should not be typed as part of any program.
In BSL, we write a unit test by typing inside parentheses check-expect and two
expressions. The first expression is the one that we wish to test and it provides what is
called the actual value. The second expression provides what is called the expected
value of the first expression.
We can now rewrite our buggy program as follows:
(+ (* 2 (sqr 4)) (* 10 4) 4)
9
You may configure DrRacket to display line numbers in the View menu.
8 What Have We Learned in This Chapter? 19
** Ex. 9 — For a rectangle displayed in Fig. 10, write a program that computes
1.The rectangle’s area
2.The rectangle’s perimeter
3.The rectangle’s diagonal distance from the lower left corner to the top right
corner
Make sure you write unit tests to validate your computed values.
You have learned to write expressions using numbers, strings, and applications.
Although this has enabled you to write programs, we need a richer set of data
types to make problem solving easier. This chapter introduces a broader set of data
types that are part of BSL. These are divided into two broad categories: primitive
and compound. A primitive data type is any type of data that is indivisible. For
example, numbers are a primitive data type. A compound data type is one that
contains multiple pieces of data. A piece of compound data can be divided into its
component parts. For example, a coordinate on the two-dimensional Cartesian plane
is compound data. Every coordinate is composed of an x and a y value. The new
data types introduced in this chapter are Boolean, symbol, character, and image. This
chapter also revisits the number and string data types to present them in more detail.
In addition, this chapter introduces how to define constants to avoid having to type
the same expression multiple times in programs.
Consider computing the area and the perimeter of the rectangles in Fig. 12.
Thinking about the problem leads us to conclude that area is computed by multiplying
the width and the length of a rectangle and perimeter is computed by adding the
doubling of the width and the doubling of the length. Based on this analysis, Fig. 13
displays a solution in the form of a program. The first thing you notice is that
comments are used to document a program. Anything after a ; is a comment in
BSL. Specifically, comments are used to document how area and perimeter are
computed. This helps the programmer and anyone else who reads the program to
understand the solution to the problem. Comments are also used to identify the
computation performed for each rectangle. Finally, unit tests are written to make
sure that the computations yield the correct values. Observe that there is one test for
each computation.
The program in Fig. 13 contains many repeated expressions. This is considered
poor programming, because repetitions are boring and lead to errors. Quite frankly,
no one wants to do the same thing over and over again. Therefore, we need a
mechanism to avoid repetitions in our programs. One idea that is promising is to
compute a value once. Instead of (typing and) evaluating each expression multiple
(a) Rectangle 1.
(c) Rectangle 3.
4 2
5
2
times, use the value of the expression multiple times. For this we need to learn a
little more BSL syntax to define variables. Variables are used to store the values of
expressions.
9 Definitions
(define i (* 4 25))
10
Some characters are not allowed such as #.
9 Definitions 23
Fig. 13 Version 1 of the area and perimeter program for the rectangles in Fig. 12
;; Area = width * height
;; Perimeter = (2 * width) + (2 * height)
These two definitions bind DELTA-X to 5 and bind i to 100. After clicking RUN, typing
the expression and pressing Enter at the prompt in the interactions area returns 5
and typing the expression i and Enter at the prompt returns 100. Defining variables
is how typing and evaluating the same expression more than once is avoided. They
store the value of an expression allowing programmers to avoid multiple evaluations
of the same expression. Instead, the defined variable is used multiple times.
By convention, two types of variables are distinguished: those whose value may
change and those whose value may not change. The latter are called constants. Both
are defined using the def syntax. The convention in this textbook is to use uppercase
letters for constants and lowercase letters otherwise. In the above example, DELTA-X
is considered a constant and i is not considered a constant. In BSL, there is no such
convention and it is up to the programmers to use this convention.
Armed with the def syntax, the program in Fig. 13 may be rewritten as displayed
in Fig. 14. Observe that for each value computed a constant is defined. Instead of
using generic variable names like x and i, the constants are given names that inform
24 2 Expressions and Data Types
Fig. 14 Version 2 of the area and perimeter program for the rectangles in Fig. 12
;; Area = width * height
;; Perimeter = (2 * width) + (2 * height)
(check-expect AREA3 4)
(check-expect PERIM3 8)
any reader what they represent. This is a good practice and it is a practice that
is followed in this textbook. It is a habit that every programmer is encouraged to
follow. The value of each constant is defined by an expression. Subsequently, the
variable is used to refer to the value of the expression instead of reevaluating the
same expression again. For instance, in the unit tests the constants are used. Observe
also that it is no longer necessary for the program to return each area and perimeter
value, which eliminates the need to clutter the interactions area with printed values.
If a value needs to be examined, type the name of the variable at the prompt and hit
Enter.
Which version of the program is clearer? Most people will argue that the program
in Fig. 14 is clearer. Any interested reader can see what the constants represent.
Furthermore, they can easily see that the tests are there to make sure that the values
of the constants are correct. Admittedly, for this rather small and simple program, it
may not seem significant. Nonetheless, it is important for you to realize now that as
the size of programs grows, it is harder for a programmer (and a reader) to remember
10 Numbers 25
all the details. Therefore, the goal of a program is twofold. First, a program must
correctly solve a problem. Second, a program must clearly communicate the solution
to the problem. Both of these goals are equally important and you must adhere to
them as responsible problem solvers. It is never acceptable to get the code to work
without documenting the solution to the problem. Think about this carefully. If you
were working on a team programming project, would you trust code that you do not
understand?
10 Numbers
In Chap. 1, a string is described as anything that is inside double quotes. The word
“anything" suggests that there may be more than one piece of data in a string. In fact,
a string is our first example of a compound data type. Compound data is constructed
using several pieces of data. For example, the string "cat" may be constructed using
the strings: "c", "a", and "t". We build a string by using double quotes. Putting cat
inside double quotes glues together "c", "a", and "t" as a single piece of data. This
is exactly the same as typing (string-append "c" "a" "t"). In other words,
the use of double quotes is simply shorthand for using string-append. Functions
that build compound data, like string-append, are called constructors.
When data is compound, it is possible to access some or all of its components.
Functions that extract a component from an instance of compound data are called
selectors. For strings, we can use the following BSL function to extract substrings:
;; string N [N] → string
;; Purpose: Extract from s the substring starting in
;; position i and ending in position j-1
(substring s i j)
The signature is stating that the function substring requires at least two inputs. The
first is a string and the second is a natural number less than or equal to the length of
s.11 The [] indicates that the third argument is optional. The optional third argument
is a natural number, j, such that 0<=j<=length of s. Think of i and j as defining
the interval [i..j) and substring as extracting the substring from position i to
position j. Not providing the third argument is shorthand for j being the length of the
s. For example, (substring "Wow!" 2) is shorthand for (substring "Wow!"
2 4). Finally, it is important to note that strings’ positions start at 0 (not 1).
The following program illustrates that strings are compound data:
11
A natural number is an integer greater than or equal to 0.
28 2 Expressions and Data Types
* Ex. 13 — There are many useful BSL functions to manipulate strings. Be-
come familiar with, for example, string-ith and string-length by looking
them up in DrRacket’s Help Desk. Write a program that appends these 3
11 Strings and Characters 29
string: "Program", "By", and "Design". Include tests to illustrate the follow-
ing:
1.The appended strings result in "Program By Design".
2.The length of the appended strings is 17.
3."P" is the substring in positions [0..1].
4.\#P is the first character of the appended strings.
5."By" is the substring in positions [8..10].
6.The last character of the appended strings is \#n
* Ex. 14 — What happens if we add the following tests to the CAT program
above? Why does each test pass or fail?
(check-expect (substring CAT 3) "")
(check-expect (substring CAT 4) "")
(check-expect (substring CAT 0 3) CAT)
(check-expect (substring CAT 0 4) CAT)
(check-expect (substring CAT -1 1) CAT)
(check-expect (substring CAT 3 1) CAT)
* Ex. 17 — Write a program that adds the lengths of "Sena" and "Isa".
30 2 Expressions and Data Types
12 Symbols
Symbols are like strings that are not decomposable. That is, symbol is a primitive
type like number. Symbols are used when we are not interested in manipulating or
accessing the characters as can be done with strings. Symbols require extending the
BSL grammar explored. The following is a new expr production rule for symbols:
expr ::= '<character>+
An expression may be a symbol, which is written as a ' followed by one or more
characters with no spaces.
DrRacket denotes most symbols as a ' followed by the name of the symbol. For
example, entering (string->symbol "Apple") at the prompt returns a symbol
that is printed as 'Apple to the screen. Entering (string->symbol "1024")
returns a symbol, but it is printed differently. Numeric symbols are printed as ',
followed by |, followed by the number, and ending with |. Therefore, the symbol for
"1024" is printed as '|1024|.
Not surprisingly, a symbol is convertible to a string and vice versa. The following
program exemplifies this observation:
;; NAME PROGRAM
Do not panic if you do not remember or recognize function composition and inverses
right now. These will be discussed in more detail later in this book. We mention them
here, because they are important in programming. For example, you may compress
and uncompress your files, encrypt and decrypt your sensitive data, or marshal–
unmarshal your data for computer communication. All of these are achieved by a
pair of functions that are inverses of each other.
** Ex. 18 — Write a program that defines a symbol for your first name and
that computes a symbol that is your first name reversed without the last letter.
Hint: Make a plan for the values that need to be computed to solve this problem.
For example, the program first converts the symbol to a string, then computes a
new string for your reversed name without the last letter, and finally computes
a symbol from that new string.
* Ex. 19 — Explain what happens and why when the following tests are added
to the name program above:
(check-expect NAME STR-NAME)
(check-expect SACH STR-NAME2)
13 Booleans
Boolean is a primitive type of data. There are only two kinds of Boolean values:
#true and #false. This type of data is important because it allows to perform
computations that depend on whether conditions hold or fail to hold. For example,
in Mathematics you have studied the absolute value function:
x if x ≥ 0
absV alx =
−x otherwise
How do you compute absVal(-100)? You plug in −100 for x, evaluate the condi-
tion, −100 ≥ 0, and determine that it is false. Since this condition is false, the value
of the function is given by −(−100). Evaluating this expression tells you that the
value of absVal(-100) is 100.
Two new expr production rules for Booleans are needed:
expr ::= #true
::= #false
These rules state that a # followed by either true or false represents a Boolean
value in BSL. Nothing else is a Boolean.
32 2 Expressions and Data Types
x y x∧y x∨y ¬x
false false false false true
false true false true true
true false false true false
true true true true false
Just like numbers have basic functions (i.e., +, -, *, and /) to create new numbers,
Booleans have the following basic functions: and (also denoted by ∧), or (also
denoted by ∨), and not (also denoted by ¬). A truth table, as the one in Table 2, is
used to illustrate the four basic Boolean functions. From the table, we can infer that
1. and is true when all inputs are true and is false otherwise;
2. or is true when any input is true and is false otherwise;
3. not is true when the input is false and is false otherwise.
The above is a complete description on the basic Boolean functions. It is noteworthy
that it is not always necessary to evaluate all the arguments to and or to or. Consider
the following two expressions:
(and #false (≤ x 20)) (or #true (> i 100))
After evaluating the first argument to and, the other arguments may be ignored,
because the result is known to be false. Similarly, after evaluating the first argument
to or, the other arguments may be ignored, because the result is known to be true.
BSL takes advantage of the fact that not all arguments to and and or may have to
be evaluated to make them faster. Instead of evaluating all the arguments to and or
or, BSL stops evaluating arguments to and after the first that evaluates to #false.
Similarly, BSL stops evaluating arguments to or after the first that evaluates to #true.
To achieve this, and and or are not functions in BSL. Instead, they are new types
of expressions. The BSL grammar includes the following two production rules for
expr:
expr ::= (and expr expr expr∗ )
::= (or expr expr expr∗ )
These production rules tell us that and and or expect at least two values as input. In
BSL, not is a one-input function.
13 Booleans 33
After running this program, all the tests pass. Observe, as displayed in Fig. 16, that
not all the expressions are evaluated. The expressions that remain unevaluated by
the tests are highlighted in black. These expressions are unevaluated, because their
value is not needed to determine the value of an and or an or expression. In fact,
there is no way to test the unevaluated code. As programmers, this means that, at
best, thorough testing gives us confidence in the program. Testing, however, never
establishes that the program is correct.
** Ex. 21 — Add the following definitions and tests to the basic Boolean func-
tions validation program:
(define AND-FERR (and #false (/ 5 0)))
(define OR-FERR (and #false (string-append "Is this "
"a Boolean?")))
13.2 Predicates
Functions that return a Boolean are called predicate functions. They are useful to
determine if the input meets some condition. In BSL, built-in types have a predicate to
determine if the input is of that type. These are the predicates for the types presented
in this chapter:
Type Predicate
number number?
string string?
character char?
symbol symbol?
boolean boolean?
image image?
For example, (string? "Hi there!") evaluates to #true and (boolean?
103167) evaluates to #false.
You have used predicates in high school Mathematics. For instance, <, >, ≥, ≤,
and = are all predicates. They are all functions in BSL with the following names: <,
>, >=, <=, and =. These functions all require 1 or more inputs. Typing (< 10) at
the prompt returns #true. Why does this make sense? It turns out that a numerical
predicate returns #true unless one of its input makes it #false. There is nothing
in < 10 that makes it #false, and therefore, this expression evaluates to true.
What does < 10121831 evaluating to #true mean? This is the BSL expression for
10 < 12 < 18 < 31. Remember that BSL expressions use prefix, not infix, notation.
It evaluates to #true, because 10 < 12 ∧ 12 < 18 ∧ 18 < 31 is true.
13 Booleans 35
Strings share an important property with numbers: they are ordinal. This means
that they may be ordered. Strings may be lexicographically ordered (i.e., alphabet-
ically ordered). Therefore, we can ask if one string is less than another or if one
string is greater than or equal to another. We cannot, however, use numerical pred-
icates with strings. The corresponding string predicates are string<?, string>?,
string<=?, string>=?, and string=?. Observe that the ? suggests that a question
is being asked. For example, (string<? "a" "b" "c") is asking if "a" comes
before "b" and "b" comes before "c".
Unlike strings, symbols are not an ordinal type. This means that there are no
functions to compare symbols like > or string<?. There are two predicates to test
symbols. The predicate symbol? tests if its input is a symbol. The predicate eq?
may be used to test if two symbols are equal. In fact, eq? does not exclusively
work with symbols. For example, we can write (eq? 1 1) and (eq? "Alpha
Centauri" "Alpha"). The first evaluates to #true and the second evaluates to
#false. Functions, like eq?, that work for many types of inputs are called generic
functions. We will explore generic functions later in this textbook. For now, we use
=, string=?, and eq?, respectively, to test for numeric, string, and symbol equality.
BSL predicates for Booleans include boolean? and false?. The first tests if its
input is a Boolean. The second tests if its input is #false. You may ask yourself,
why is true? not a predicate in BSL? The answer is that the creators of BSL cannot
possibly implement every conceivable function. Therefore, they must choose what
functions to provide. One criterion that is used to decide is whether or not a value
may be computed using other functions. Can we write expressions to determine if
a variable is #true? To answer this question, we must carefully think about what it
means to be #true. First, the value must be a Boolean. Second, the value must not
be #false. With this insight, we can write an expression to determine if a variable is
true using not, and, and false?. This is a sample program to test our observations:
(define A-STRING "This is not true.")
(define A-SYMBOL 'not-true)
(define A-NUMBER 87)
(define A-BOOL #true)
(define A-BOOL2 #false)
* Ex. 22 — BSL provides many useful predicates to us. What are the signature
and purpose of the following predicates:
1. odd?
2. even?
3. positive?
4. negative?
5. string-uppercase?
6. string-lowercase?
7. string-numeric?
8. string-alphabetic?
9. string-contains?
*** Ex. 25 — The nand (not and) boolean function has a property called func-
tional completeness. This means that any Boolean expression may be written
only using nand expressions. The truth table for nand is
A B (nand A B)
false false true
false true true
true false true
true true false
The value of nand is given by the expression (not (and A B)). We can
compute (not X) using a nand expression as follows: (not (and X X)).
14 Images 37
Write a program to demonstrate how to compute and and or using only nand
expressions.
14 Images
This teachpack contains many functions that are split into three broad categories:
Basic Constructors These are functions to create basic images like circles, rectan-
gles, ellipses, stars, and text.
Property Selectors These are functions to extract properties of images such as
width and length.
Image Composers These are functions that combine existing images to create new
images.
It is useful to understand two basic definitions before using the 2htdp/image
teachpack:
mode This refers to how basic geometric shapes are drawn: not filled or filled. Use
'outline or "outline" when an outline of a shape is desired. Use 'solid or
"solid" when a solid shape is desired.
image color This is a string or a symbol representing the desired color. Search for
image-color? in DrRacket’s Help Desk to see the list of predefined colors.
Figure 17 illustrates outline and solid geometric shapes. Figure 17a displays an
outline red rectangle created using the rectangle function. Figure 17b displays a
solid green circle created using the circle function.
To illustrate the use of the basic image constructors, consider the program in Fig. 18
to construct the images in Fig. 17. First, constants for the characteristics of the
rectangle and the circle are defined. Next, a constant for each image is defined. The
rectangle is created using the basic constructor rectangle. It requires four inputs: a
width, a height, a mode, and a color. The circle is created using the basic constructor
circle. It requires three inputs: a radius, a mode, and a color. This is followed by
tests. We can write the expected value of a test using a function application or an
image given that both are valid expressions in BSL. The tests that use images require
that the image be created first and then copied and pasted as part of the test.
There are too many basic image constructors to describe them all in this textbook.
Therefore, search for 2htdp/image in the Help Desk to learn about and experiment
with them. Be creative!
(check-expect RED-OUTLINE-RECT )
(check-expect GREEN-SOLID-CIRCLE (circle 25 solid green))
(check-expect GREEN-SOLID-CIRCLE )
Consider the problem of computing half the width, half the height, and the number
of pixels for the following images:
(define A-STAR-IMG (radial-star 10 5 35 'outline 'gold))
(define A-RHOMBUS-IMG (rhombus 60 75 'solid 'red))
The first step is to clearly identify what needs to be computed and how it is computed.
We need to compute three values as follows:
Half the Width Divide the image’s width by 2.
Half the Height Divide the image’s height by 2.
Number of Pixels Compute the image’s area.
The next question that arises is how are we to test our computations. We know that
every image is rectangular, but this does not tell us what is the width or the height
of a star image or of a rhombus image. When we do not know what a specific value
ought to be or it is too difficult to write a specific value, we can use property-based
testing to write tests. Instead of testing for the specific value of an expression, we
test properties that the value of the expression ought to have. For our problem, we
40 2 Expressions and Data Types
;; Star computations
(define STAR-W (image-width A-STAR-IMG))
(define STAR-H (image-height A-STAR-IMG))
(define STAR-HALF-W (/ STAR-W 2))
(define STAR-HALF-H (/ STAR-H 2))
(define STAR-PIXELS (* STAR-W STAR-H))
;; Star tests
(check-expect (* 2 STAR-HALF-W) STAR-W)
(check-expect (* 2 STAR-HALF-H) STAR-H)
(check-expect (/ STAR-PIXELS STAR-W) STAR-H)
(check-expect (/ STAR-PIXELS STAR-H) STAR-W)
;; Rhombus computations
(define RHOMBUS-W (image-width A-RHOMBUS-IMG))
(define RHOMBUS-H (image-height A-RHOMBUS-IMG))
(define RHOMBUS-HALF-W (/ RHOMBUS-W 2))
(define RHOMBUS-HALF-H (/ RHOMBUS-H 2))
(define RHOMBUS-PIXELS (* RHOMBUS-W RHOMBUS-H))
;; Rhombus tests
(check-expect (* 2 RHOMBUS-HALF-W) RHOMBUS-W)
(check-expect (* 2 RHOMBUS-HALF-H) RHOMBUS-H)
(check-expect (/ RHOMBUS-PIXELS RHOMBUS-W) RHOMBUS-H)
(check-expect (/ RHOMBUS-PIXELS RHOMBUS-H) RHOMBUS-W)
know that an image is always rectangular. This means that twice half the width ought
to be the width, twice half the height ought to be the height, the number of pixels
divided by the image’s height ought to be the image’s width, and the number of
pixels divided by the image’s width ought to be the image’s height.
Now that we have a game plan, we can proceed to write our program as displayed
in Fig. 19. Observe that related definitions and tests are kept together. First, a data
item is defined like A-RHOMBUS-IMG. Second, the result of computations is defined
like RHOMBUS-HALF-W. Third, tests are written. Organizing code in such a manner
is a good practice because anyone who reads the code (including yourself 6 months
from now) will be able to understand the goals.
14 Images 41
Image composers are used to create new images from existing images. The image
teachpack provides a myriad of such functions and you ought to experiment with
them as you read the documentation in the Help Desk.
Consider the problem of creating the image in Fig. 20. As always, the first step
is to determine what needs to be computed. The image consists of 6 nested squares.
The largest square is yellow and at the bottom. The smallest square is blue and on
the top. This suggests a divide and conquer strategy. First, compute all the needed
squares. Second, overlay the squares to create the desired image. We can test the
result using property-based testing. A program to implement this strategy is
;; Tests
;; NOTE: SQUARE0 is the largest square.
(check-expect (image-width NESTED-SQS) (image-width SQUARE0))
;; The squares
(define SQUARE0 (square 60 'solid 'yellow)) ;; largest square
(define SQUARE1 (square 50 'solid 'blue))
(define SQUARE2 (square 40 'solid 'yellow))
(define SQUARE3 (square 30 'solid 'blue))
(define SQUARE4 (square 20 'solid 'yellow))
(define SQUARE5 (square 10 'solid 'blue)) ;; smallest square
the 6 needed squares from largest to smallest. Finally, the needed image is created
by using the function overlay always placing a smaller image over a larger image.
It is natural to wonder how the placing is done. Recall that an image consists of
many pixels. One of these pixels serves as an anchor point around which images
are placed. Unless otherwise specified, image composing functions use the pixel at
the center of the image as the anchor point. This means, for example, that overlay
places all the center points of the images one on top of another. In our program,
the anchor point of SQUARE1 is placed over the anchor point of SQUARE0. Next,
the anchor point of SQUARE2 is placed over the anchor point of the SQUARE1 and
SQUARE0 composed image, and so on until all the squares are overlayed. You may
contrast this behavior with the behavior of the application expression:
(overlay/xy img1 i j img2)
For overlay/xy, the images start aligned with respect to their top-left corner pixel
(not the center pixel). The function overlays img1 over img2 but moves img2 i pixels
to the right and j pixels down. If i is negative, img2 is moved to the left. If y is
negative, img2 is moved up. Experiment with image composers!
Another noteworthy characteristic of our program is that the testing is not very
satisfying. Consider what happens if we mistakenly define NESTED-SQS as follows:
(define NESTED-SQS (overlay SQUARE0 SQUARE1 SQUARE2
SQUARE3 SQUARE4 SQUARE5))
No tests fail when running the program despite that NESTED-SQS is the image of
a yellow square. Clearly, this is not the desired result and this testing shortcoming
ought to be addressed. To remedy such a situation, add unit tests using actual values
that are computed. In this case, first experiment with our program. Once the desired
image is computed, copy and paste it as the expected result for testing NESTED-SQS.
In this manner, any reader of your code can see that both your model and your result
are consistent with expectations. You must be very careful when employing such
a strategy to write tests. If a computed value is wrong, then its use in a test will
make the test pass. Your program, however, still computes the wrong value. It is best
to limit the use of this strategy to test computed images that are easy to visually
validate.
There are two image composing functions that are used extensively to create anima-
tions and video games:
empty-scene Creates an empty image of some given width and height. Option-
ally, the color of the empty image may be provided.
place-image Places the first given image at the given x and y coordinates in the
second given image.
As characters and elements change in a video game, the displayed image must change.
This is done by computing a new image. The empty image is used to create the base
14 Images 43
image of the video game or animation. Think of this base image as the empty canvas
of an artist. The function place-image is used to place elements.
To properly use place-image, a brief description of the computer graphics coor-
dinate system is needed. Figure 21 displays the coordinate system used in computer
graphics. The possible values of x grow from left to right. To place an image further
to the right, the x value is increased. To place an image further to the left the x value
is decreased. The possible values of y grow from top to bottom. To place an image
further down, the y value is increased. To place an image further up, the y value is
decreased. For a given image with dimensions WIDTH x HEIGHT, the valid x values
are in [0..WIDTH-1] and the valid y values are in [0..HEIGHT-1]. If any part of
a placed image falls outside these coordinate ranges, the image is cropped and parts
of the placed image do not appear in the result.
Consider the problem of creating an image with three different alien ships in a
black empty scene. To make the task more manageable, use a divide and conquer
strategy:
• Define the dimensions of the empty scene.
• Compute three different alien ships.
• Compute an image with one alien ship.
• Compute an image with two alien ships.
• Compute an image with three alien ships.
• Write tests.
There are an infinite number of ways a program can be written following this strategy.
Figure 22a displays a proposed partial solution. A black empty scene, 200 x 100,
and 3 different alien ships that differ in color are defined. After this, 3 images are
defined using place-image. Each successive image contains one more ship.
The resulting image with the 3 alien ships is displayed in Fig. 22b. Immediately,
you can see there is a bug in the program. The pink alien ship is cropped and only
part of it appears in the image. This example highlights the value of testing. The
program in Fig. 22a does not contain a test. Although the divide and conquer strategy
is a good one, it is always necessary to perform thorough testing to establish a degree
of confidence in a problem’s solution.
44 2 Expressions and Data Types
Fig. 22 Program for an image with 3 different alien ships in a black empty image
Program.
(require 2htdp/image)
** Ex. 27 — Debug and complete the design of the program in Fig. 22a. Make
sure to test all defined values.
* Ex. 28 — Write a program that creates an image of a banner with your name
on it.
* Ex. 29 — Write a program that creates an image of a triangle that is over an
image of the triangle flipped over the x-axis.
*** Ex. 31 — Write a program that creates a starry night image. Your com-
position must have a representation of at least one moon, three stars, and a
stickman. Figure 23aa and 23bb display sample starry night images created by
students. Be creative and have fun!
a
Courtesy of Lindsey M Reams.
b
Courtesy of Shamil Dzhatdoyev.
• When solving a problem, outline the values that must be computed to guide the
development of a solution based on divide and conquer.
• In addition to number and string, BSL includes the character, symbol, Boolean,
and image types.
• Primitive type instances, like a Boolean, cannot be decomposed.
• Compound type instances, like a string, can be decomposed.
• The BSL syntax developed so far is displayed in Fig. 24.
– New expressions for characters, symbols, Booleans, and images.
– andand orexpressions are syntax for the Boolean functions and and or.
– not is a Boolean function in BSL.
– | means or. For example, the first production rule for expr states that it can be
a number, a string, or a variable.
• Computations involving numbers and images may contain errors.
• Use check-within to test the computation of a real number.
• Boolean functions are called predicates and allow programmers to determine if
the input satisfies a property.
• Model checking tests properties of a computed value.
• The 2htdp/image teachpack provides functions to create, compose, and extract
properties of images.
Chapter 3
The Nature of Functions
Every high school student has studied functions. What for? Have you ever wondered
where functions come from? Why do they exist? Certainly, many students can state
that a function is a mapping from a domain element to a range element. That,
however, does not tell anyone where functions come from. Perhaps, looking at an
example helps. Consider the following function:
f(x) = x2 + 3x - 10
Certainly, many students can state that x is an input element from the domain and
that the value of f(10) is 120. Now, we know something about functions. We can
use functions to compute values. This is done by "plugging in" 10 for x. That is, all
instances of x are substituted with 10 to obtain
f(10) = 102 + 3·10 - 10
Now, it is all a matter of evaluating function’s body (i.e., the expression to the right of
the equal sign). This is screaming to us that functions are likely to play an important
role in programming. After all, BSL, as well as other programming languages, is
exceptionally good at evaluating expressions. A program in BSL to evaluate f(10)
looks like this:
;; Evaluating f(x) = x^2 + 3x - 10
(define F-X10 (+ (sqr 10) (* 3 10) -10))
(check-expect F-X10 120)
We still, however, do not know where f(x) came from.
This chapter introduces the process of abstraction over expressions that leads to
functions. It also introduces the BSL syntax needed to define functions (like f(x)).
More importantly, it introduces the first design recipe to guide problem solvers in
function development. A design recipe is a series of steps, each with a specific
outcome, that defines a systematic process to implement functions. A design recipe
does not tell you the solution to a problem. That you must figure out. It does, however,
prove useful to take you from a problem statement and a blank IDE to a program that
solves the problem in a manner that is understandable to others.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 47
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_3
48 3 The Nature of Functions
Writing a program to compute the value of f(10) has proven rather easy. Now you
are asked to also have your program compute f(0), f(1), f(20), and f(100).
After some thought and a bit of typing, your program looks as displayed in Fig. 25.
A different constant is defined for each value of f(x) obtained from a different value
of x. The tests validate that each value of f(x) has been correctly computed.
Now, you are asked to have your program compute all the values of f(x) for the
integers in [11..19]. This is another 9 different values of x and, frankly speaking,
many are unlikely to want to do a lot of the same typing over and over again. To
avoid this, some may decide to copy, paste, and edit code to account for the changes.
This is dangerous, because it is error-prone. It also means that for a new batch of x
values you will be doing a lot of copying and pasting again. There must be a better
way to get the job done.
Why would anyone decide to cut and paste to write more code? It turns out there
is something interesting about the expressions used to compute the different values
of f(x). Observe that they are almost identical (see Fig. 26). There is only one
difference from one expression to the next: the number plugged in. The elements
that vary among similar expressions are called variables. That is, we can abstract
away the differences among similar expressions by associating each difference with
a variable. For the expressions above, let us call the single difference x (as done in
the mathematical function f(x)). This reduces all the expressions above to a single
expression:
(+ (sqr x) (* 3 x) -10))
In order to exploit such an expression, a mechanism is needed to provide x with a
value.
Let us go back and see how mathematicians provided this mechanism. They chose
the syntax f(x) = e, where e is a mathematical expression. We say that f(x) is
the function header. A function header contains two parts: the name of the function
and the name of the input variables. The input variables are called the function’s
parameters. For f(x), f is the function name and x is the only parameter. We say that
16 The Rise of Functions 49
;; number number
;; Purpose: To compute f(x)
(define (f x)
(+ (sqr x) (* 3 x) -10))
• Instead of asking you to modify the program for every value of f(x) needed, can
others use the program to compute these values?
To answer the first question, consider a single test. Most individuals find more
readable and prefer to type
(check-expect (f 11) 144)
over
(check-expect F-X11 144)
(define F-X11 (+ (sqr 11) (* 3 1) -10))
The answer to the second question is yes given enough memory. As a program is
evaluated, memory is allocated to store values. Since memory is finite, it is possible
that a program may be asked to compute a value that requires more memory than
is available. This is something to keep in mind. In practice, however, people around
the world execute programs every day without running out of memory. The answer
to the third question is an unequivocal yes. Anyone running the program only needs
to type the correct application expression at the prompt in the interactions area.
Why were the tests using constants not rewritten to eliminate the need to define
the constants? The answer is that many times it is useful to see how a value is
17 General Design Recipe for Functions 51
computed to understand a program. Therefore, you should write both types of tests:
tests that use concrete values (like the last 9 tests) and tests that use defined constants
for expressions that show how a value is computed (like the first 4 tests).
is computed by the function. The function header is written to satisfy Step 5. Pick a
function name that helps identify the purpose of the function. For example, naming
a cubing function f is a poor choice. A better choice is naming the function cube. In
addition, the parameters from Step 3 must be written in the order that corresponds
to the types in the signature. For Step 6, tests are written using an application of
the function as the tested value and using both the constants from Step 2 and other
values not defined as constants. The body of the function is developed to satisfy
Step 7. β-reduction is performed on any one of the sample expressions. That is, the
differences are substituted with the correct parameter. Finally, Step 8 requires the
running of the tests. If all the tests pass, you may have guarded optimism in having
designed a correct function. If there are any errors, then you need to check your
answers to each step of the design recipe.
The design recipe suggests a function template may be used for function design.
A function template is like a skeleton for functions. It captures the main components
a problem solution needs. Figure 29 displays the basic function template. Every time
a problem is being solved, the template is copied and specialized. When an answer
for a step of the design recipe is formulated, the copied template is edited to reflect
the answer.
17 General Design Recipe for Functions 53
To illustrate the use of the design recipe, consider the problem of creating name
banner images for Craig, Julia (Ohio), Steve, Little Nick, Big Nick, and Jeremy. The
name image should contain only the name in a green 36 size font.
The answers to each step of the design recipe are outlined as follows:
STEP 1
Represent each name as a string. The banner image is computed by applying the
text function to a name, 36, and 'olive.
STEP 2
Three constants for the value of sample expressions are
STEP 3
The only difference among the three sample expressions is the string representing
the name. The variable for this difference is name.
STEP 4
There is only one difference which is a string and the value computed is an image.
The signature and purpose statement are
;; string → image
;; Purpose: Create a banner for the given name
;; using font 36 and the color olive
STEP 5
The function header contains only one parameter, because a single difference was
identified in Step 3. The name make-banner is suggestive of the purpose of the
function. The function header is
(define (make-banner name)
STEP 6
The tests using the defined constants and sample values are below.
;; string image
;; Purpose: Create a banner for the give name using
;; font 36 and the color olive
(define (make-banner name)
(text name 36 olive))
)
(check-expect (make-banner "Big Nick")
)
(check-expect (make-banner "Jeremy")
STEP 7
The body of the function uses name instead of a concrete string:
(text name 36 'olive)
STEP 8
All tests pass.
Figure 30 displays the full program written in BSL inside DrRacket.
18 Auxiliary Functions 55
* Ex. 32 — Follow the steps of the design recipe to write a program to compute
the area of an ellipse.
** Ex. 33 — Follow the steps of the design recipe to write a program to com-
pute the image of a smiley face.
* Ex. 35 — Follow the steps of the design recipe to write a program to deter-
mine if a string is of even length.
18 Auxiliary Functions
Seldomly, programs only need to compute many instances of a single value as the
program in Fig. 30. It is far more common to have to compute several different values
to solve a problem. A good designer develops a function for each different value.
Such a practice makes it easier for the programmer and for others to understand
the solution to the problem. It also endows programs with modularity. Modularity
enables re-usability of expressions and reduces code duplication. In other words,
modularity is tightly connected to abstraction.
If problem analysis reveals that different kinds of values need to be computed,
consider which values ought to be computed by an auxiliary function. If special
knowledge is required to compute a value or if different instances of the same value
are needed, then designing an auxiliary function is a good choice. On the other hand,
if the expression needed to compute a new value is short, simple, and used only once,
then designing a new function may add little clarity to the solution.
If a programmer determines that several functions are needed, she can follow
two basic strategies: bottom-up or top-down. Bottom-up is a programming style in
which the simplest (usually the smallest) functions are designed first, and then these
functions are used to design more complex functions. Top-down is a programming
style in which the complex functions are designed first, and then simpler functions are
designed. The most commonly used approach is top-down, because a programmer
rarely knows in advance all the simpler functions that are required. Following a top-
down approach allows the programmer to discover, as the design advances, needed
simpler functions.
56 3 The Nature of Functions
STEP 3
The only difference among the sample expressions is the radius. The variable for
this difference is a-radius.
18 Auxiliary Functions 57
STEP 4
The signature and purpose statement are
;; R>= 0 → R>= 0
;; Purpose: Compute the area of the circle with the given
;; radius.
STEP 5
The function header is
(define (area-circle a-radius)
Observe that the chosen function name suggests the purpose of the function.
STEP 6
The tests using defined constants and sample values are
Observe that the tests include, 0, the lowest possible value for a-radius. It is
always good practice to test border values in the domain of a function.
STEP 7
The body of the function is
(* pi (sqr a-radius))
STEP 8
All tests pass.
With area-circle designed, implemented, and tested, the function for the area
of a washer is designed. The steps of the design recipe may be satisfied as follows:
STEP 1
The area of a washer is given by the area difference of its outer and inner circles.
STEP 2
Three constants for the value of sample expressions are
Observe that function composition is used to write the tests. The output of
area-circle is used as input to -. In addition, observe that the constant names
are suggestive of the value they represent.
STEP 3
There are two differences in the sample expressions: the radii of the outer and the
inner circles named, respectively, outer-radius and inner-radius.
STEP 4
The signature and purpose statement are
The name of the function is suggestive of its purpose. Compare this name choice
with a name like g or area.
STEP 6
The tests using defined constants and sample values are
STEP 7
The body of the function is
(- (area-circle outer-radius) (area-circle inner-radius))
STEP 8
All tests pass.
The complete program is displayed in Fig. 32. An advantage of bottom-up design
is that functions may be tested as soon as they are implemented. For example, the
area-circle function may be tested as soon as it is written.
60 3 The Nature of Functions
* Ex. 36 — Use a bottom-up approach following the steps of the design recipe
to write a program to determine if f(a) equals f(b), where f(x) = x2 - 9.
*** Ex. 37 — Use a bottom-up approach, following the steps of the design
recipe, to write a program to implement not, or, and and using only nor (not
or). The truth table for nor is displayed in Fig. 33. For example, (or A B) =
(nor (nor A B) (nor A B)).
* Ex. 38 — Use a bottom-up approach following the steps of the design recipe
to write a program to determine if two strings of length 3 contain a.
14
A compiler is a program that converts programs written in a language like BSL into machine
code that is executable by a computer.
19 Top-Down Design 61
* Ex. 39 — Use a bottom-up approach following the steps of the design recipe
to design and write a program to compute the flag images of Ghana, Honduras,
Lithuania, Niger, and Syria.
19 Top-Down Design
To illustrate the top-down design process, consider the problem of computing images
like those in Fig. 34. Each image has 4 overlayed squares at a 45◦ angle. Instead of
first trying to figure out how to compute each square in the image composition, start
by writing expressions to compute the image composition. For unknown values,
like the length of a square, use a constant. The definitions of these constants are a
different problem from the problem of creating the desired image. In other words,
use a divide and conquer strategy.
Under this design approach, the steps of the design recipe may be satisfied as
follows:
STEP 1
The image is computed by overlaying squares of alternating colors and differing
lengths. Every other overlayed square, starting with the largest square, is rotated
by 45◦ .
STEP 2
Observe that it is already known that several squares of different colors and lengths
are needed. This immediately suggests that we abstract away the image compu-
tation for squares and design (later) a function for this purpose. Furthermore, we
know that this function requires two arguments (one for each difference): a length
and a color.
Constants for the value of sample expressions may be defined as follows:
Observe that we do not concern ourselves at this point with how make-sqr is
implemented or with how to define the different lengths. We assume that they can
all be defined.
STEP 3
There are 6 differences in the sample expressions: 4 lengths and two colors. We
name the differences: bot-len, len2, len3, top-len, botclr, and topclr.
STEP 4
The signature and purpose statement are
STEP 5
The function header is
(define (make-nested-sqs bot-len len2 len3 top-len
botclr topclr)
19 Top-Down Design 63
STEP 6
The tests using defined constants and sample values are
)
(check-expect (make-nested-sqs 20 14.14 9.99 7.06 'pink 'purple)
STEP 7
The body of the function is
STEP 8
All tests pass.
At this point, we cannot run any tests because make-sqr and the length constants
are undefined. The answer written for Step 8 assumes that the tests pass after all
required functions and constants are defined. If all tests do not pass, then checking
the steps of the design is necessary.
Let us turn our focus now to the design and implementation of make-sqr. We
arrive to this function with some clear ideas about the abstraction. There are two
differences: side length and color. If we were to discover that more values are needed,
64 3 The Nature of Functions
then we have a signal that there is a bug in our abstraction. We proceed assuming
that the function can be designed with the stated differences.
The steps of the design recipe may be satisfied as follows:
STEP 1
The image of a solid square is computed using the square function and the given
side length and color.
STEP 2
Constants for the value of sample expressions may be defined as follows:
STEP 3
There are 2 differences in the sample expressions as expected: a length and a
color. We name the differences: side-len and a-color.
STEP 4
The signature and purpose statement are
STEP 5
The function header is
(define (make-nested-sqs botlen len2 len3 toplen
botclr topclr)
STEP 6
The tests using defined constants and sample values are
Fig. 35 The length of the next smaller square is the length of a hypotenuse
STEP 7
The body of the function is
STEP 8
All tests pass.
The next task is to define the constants for the lengths. For any image of nested
squares, the length of the largest square is arbitrary. For example, the length of the
largest square can be 100. The length of the next smaller square is not arbitrary as
it depends on the length of the outer square. How do we compute the length of the
next smaller square? The problem must be carefully analyzed. Figure 35 displays the
placement of a smaller square inside a larger square. The length of the outer square
is w. Observe that two adjacent midpoints and the center of the outer square form
a right triangle. The lengths of the sides forming the 90◦ are both w2 . The length of
the inner square is the length of the hypotenuse of the right triangle. We know that
the length of the hypotenuse is given by the Pythagorean theorem:
x2 = ( w2 )2 + ( w2 )2
x = ( w2 )2 + ( w2 )2
= 2 * ( w2 )2
2
= 2 * w4
w2
= 2
66 3 The Nature of Functions
A function can be defined to compute the length of inner squares. The only difference
in computing one inner square length from another is the length of the outer square.
Instead of abstracting over similar expressions, we have developed a mathematical
expression for the length of an inner circle that is easily translated to BSL. Both
approaches to converging on an expression for the body of a function are valid and
ought to be part of any problem solver’s toolbox.
The steps of the design recipe may be satisfied as follows:
STEP 1
The length of the next smallest square is equal to the length of the line between
the two midpoints of two adjacent sides of the
enclosing square of length w. The
2
length of the next smaller square is given by w2 .
STEP 2
Constants for the value of sample expressions may be defined as follows:
STEP 3
There is a single difference in the sample expressions as expected: a length. We
name the difference side-len.
STEP 4
The signature and purpose statement are
;; R>= 0 → R>= 0
;; Purpose: Compute the length of the next smallest square
STEP 5
The function header is
(define (compute-new-sqr-len side-len)
STEP 6
The tests using defined constants and sample values are
Given that constants for the square lengths for a second image are needed, the
function being designed may be used to compute these. The tests using sample
values based on these constants may be written as follows:
;; Tests using sample values
(define IMG2-LEN1 150)
(define IMG2-LEN2 (compute-new-sqr-len IMG2-LEN1))
(define IMG2-LEN3 (compute-new-sqr-len IMG2-LEN2))
(define IMG2-LEN4 (compute-new-sqr-len IMG2-LEN3))
STEP 7
The body of the function is
STEP 8
All tests pass.
An important point to highlight is when a defined function may be applied in BSL.
A defined function may be applied any time when it is part of a test. All other times,
however, a function must be defined before it is applied to any arguments. That is,
the definition of a function must appear in a program before its first use outside tests.
* Ex. 41 — Use a top-down approach following the steps of the design recipe
to write a program to compute the flag images of Armenia, Austria, Bulgaria,
Colombia, and Estonia.
* Ex. 42 — Use a top-down approach following the steps of the design recipe to
write a program to compute f(x) = 4(x3 - 1)3 - 6(x3 -1)2 + 2(x3 -1)
+ 8.
68 3 The Nature of Functions
* Ex. 43 — Use a top-down approach following the steps of the design recipe
to write a program to append two double copies of a string separated by a space.
For example, "abc" results in "abcabc abcabc".
*** Ex. 44 — Use a top-down approach following the steps of the design recipe
to write a program to compute ((a ⇒ b) ∧ (b ⇒ c)) ⇒ (a ⇒ c). i
⇒ j is ¬i ∨ j.
It is time to put your newly acquired skills to work. This chapter develops the first
version of a video game that we shall call Aliens Attack. This chapter, however, will
not develop a full video game. You need to learn more about problem solving and
more BSL syntax to write a fully working video game. Therefore, the video game
is developed using the process of iterative refinement. That is, as you learn more
about problem solving and about BSL syntax, a more complete video game shall be
developed. This process culminates with a multiplayer game that you may play with
your friends over the internet. That is a promise. If you work hard and absorb all the
lessons contained in this textbook, then you will acquire enough skills to develop a
multiplayer video game.
Figure 37 displays an image of a single-player version of Aliens Attack. In the
scene there is an army of aliens attacking earth and there is a single rocket defending
earth. There is no placating the aliens and earth must defend itself. The rocket is at
the bottom of the scene over earth and may move left to right without going off the
edge of the image. The rocket may also shoot in an attempt to destroy the aliens.
The army of aliens starts toward the top of the scene. All the aliens move in the
same direction: right, left, or down. The aliens move right one step at a time until
an alien reaches the right edge of the scene. At this point all the aliens move down
a step. After moving down a step, the aliens move left one step at a time until an
alien reaches the left edge of the scene. The aliens then move down a step. This cycle
continues until an alien reaches and conquers earth (the player loses the game) or all
the aliens are destroyed (the player wins the game). Observe that, like the rocket, the
aliens may not go off the scene as they move. When the rocket shoots, a shot starts
at the position of the rocket and moves up until it hits an alien or it goes off the top
each of the scene.
The first version of Aliens Attack is not truly a video game. That is fine because
it will be refined. To start, the focus is on problems that you can solve with the skills
you have already developed. The development of Aliens Attack version 0 focuses on
the creation and placement of images to render the video game as an image. This is
a good starting point because you know the basics of creating and placing images
and of writing functions. The first goal is to design the scene where the images of
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 71
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_4
72 4 Aliens Attack Version 0
the game are rendered. The second goal is to develop functions to create the images
of the rocket, the alien, and the shot. The third goal is to create functions to draw the
elements in a scene.
Recall that graphics are created using a coordinate system in which the x coordinate
grows from left to right and the y coordinate grows from top to bottom. Also recall
that a w1 x h1 empty scene into which images are placed may be created as follows:
(empty-scene w1 h1 )
Creating an empty scene is only part of what is needed. The question that immediately
arises is how do we reason about this empty scene. Figure 38a displays the view of
the empty scene as a grid of pixels. Each square in the grid is a pixel and items, like
the blue square, are placed in the scene using pixel coordinates. The blue square in
Fig. 38a is at pixel coordinate (15, 19). This can work well but seems to mismatch
our goal. Our goal is to place images in a scene, not manipulate pixels. If we choose
a maximum size for each image in the video game, we can reason about the empty
scene as boxes where an image fits. Figure 38b displays this view of the empty scene.
A square is placed using image coordinates, not pixel coordinates. For instance, the
blue square in Fig. 38a is at image coordinate (3,3).
21 The Scene for Aliens Attack 73
So, why should we care about the perspective we adopt? It turns out that data
representation strongly influences how a problem is reasoned about. Consider, for
example, how is an alien hit by a shot is detected. We shall first analyze the problem
using the pixel perspective of the game’s scene. Assume that the image of an alien
has dimensions W x H as illustrated in Fig. 39 and the alien is located at pixel
coordinates (x, y). We define a hit as the shot’s position being inside the alien’s
image. That is, if the shot’s position is any pixel covered by the alien’s image, then
the alien is hit. In Fig. 39, for example, the shots at (xr1 ,yr1 ) and at (xr2 ,yr2 ) have
hit the alien. The shots at (xr3 ,yr3 ) and (xr4 ,yr4 ) have not hit the alien. Now,
how is a hit detected? After thinking about it, you can see that a hit means that the
shot can be at most half the alien’s image width (i.e., W/2) away on the x-axis and
at most half the alien’s image height (i.e., H/2) away on the y-axis from the alien’s
coordinates (i.e., (x, y)). This reasoning leads to a function to detect a hit alien
that looks as follows:
(define HALF-ALIEN-IMG-WIDTH (/ (image-width FUEL-IMG) 2))
1 1
74 4 Aliens Attack Version 0
H/2
H/2
This function is much simpler and easier to understand than the first. The important
lesson here is that different representations, like pixel coordinates versus image
coordinates, may lead to vastly different solutions/programs. It is worth exploring
different representations to determine if any lead to a simpler and easier to understand
solution/program.
Given that we have carefully analyzed how to represent coordinates in the game’s
scene, we may be cautiously confident that using image coordinates leads to simpler
problem solutions. We can also surmise that we need to be careful about defining
the data representation in the program. For this, we need data definitions. A data
definition is written to describe a new type of data. For Aliens Attack, the following
data definitions and constants are used:
;; Character Image Maximum Dimensions
(define IMAGE-WIDTH 30)
(define IMAGE-HEIGHT 30)
#|DATA DEFINITIONS
A character image (ci) is an image which is at most
IMAGE-WIDTH x IMAGE-HEIGHT pixels
21 The Scene for Aliens Attack 75
;; Sample image-x
(define AN-IMG-X (/ MAX-CHARS-HORIZONTAL 2))
(define MIN-IMG-X 0)
(define MAX-IMG-X (sub1 MAX-CHARS-HORIZONTAL))
;; Sample image-y
(define AN-IMG-Y (/ MAX-CHARS-VERTICAL 2))
(define MIN-IMG-Y 0)
(define MAX-IMG-Y (sub1 MAX-CHARS-VERTICAL))
Creating the images needed for Aliens Attack provides the opportunity to practice
the steps of the design recipe. In addition, it provides us with the opportunity to write
an application programming interface (API). An API defines the data formats, the
conventions, and the functions that may be used to create and access data. For Aliens
Attack images, the API is rather simple: the dimensions of any character image must
be 30 x 30 pixels or less and must be created using functions in the image teachpack.
One of the benefits of using or creating an API is information hiding. This means,
for example, that the implementation details of a function are hidden from any
programmer who uses the function. A programmer uses the function independent of
how the function is implemented. To be successful, the signature and the purpose of
the function must be clear. Otherwise, programmers will not know how to properly
use a function.
Observe that according to the data definitions, every ci is an image. Every image,
however, is not a ci. A natural question that arises is determining if an image is a
ci. Testing image values is useless, because everybody’s idea of a shot, an alien, or
a rocket is different. Furthermore, the data definition ci only specifies ci properties
(not values). This is a clear situation where property-based testing is needed. For
this, a predicate to determine if a given image is a ci is needed. Following the steps
of the design recipe, the predicate may be designed as follows:
STEP 1
Determine that both the image width is less than or equal to IMAGE-WIDTH and
that the image height is less than or equal to IMAGE-HEIGHT.
STEP 2
Definitions for the value of sample expressions are
(define IS-CI
(and (<= (image-width (circle 10 'solid 'red))
IMAGE-WIDTH)
(<= (image-height (circle 10 'solid 'red))
IMAGE-HEIGHT)))
(define NOT-CI
(and (<= (image-width (square 40 'solid 'blue))
IMAGE-WIDTH)
(<= (image-height (square 40 'solid 'blue))
IMAGE-HEIGHT)))
(define NOT-CI2
(and (<= (image-width
(rectangle 2 50 'solid 'blue))
IMAGE-WIDTH)
22 Creating Aliens Attack Images 77
(<= (image-height
(rectangle 2 50 'solid 'blue))
IMAGE-HEIGHT)))
Observe that sample expressions are developed for both images that are and that
are not ci.
STEP 3
The only difference among the sample expressions is the image tested. The
variable for this difference is an-img.
STEP 4
The signature and purpose statement are
;; image → Boolean
;; Purpose: To determine if the given image is a ci
STEP 5
The function header is
(define (ci? an-img)
STEP 6
Testing may be satisfied as follows:
Observe that both images that are and are not ci are tested.
STEP 7
The body of the function is
STEP 8
All tests pass.
Recall that this step is only satisfied when the tests are executed and pass.
78 4 Aliens Attack Version 0
Now that ci and non-ci images are distinguishable, attention turns to the con-
struction of cis. There are three image constructors that are needed: one for a shot,
one for an alien, and one for a rocket. The development presented here is only illustra-
tive. This is your video game and you are encouraged to be creative and personalize
the game to your liking. Understand the principles for developing the images and then
design functions for your own images. Remember that, unlike homework problems
in high school Mathematics, there may be many solutions to a problem.
23 Shot Image
The image of the shot is the simplest one in Fig. 37. The shot’s image is a radial
star and the color is orange. These, of course, may be different depending on your
preferences. The goal is to design a constructor for shot images. The steps of the
design recipe may be satisfied as follows:
STEP 1
A shot image is a ci of a radial star created using the function radial-star.
Observe that the data definition of a shot image is described in terms of the type
for character images previously developed.
STEP 2
Definitions for the value of sample expressions are
The constants are defined to facilitate changing the color of sample shots. Observe
that the outer diameter of the radial star is IMAGE-WIDTH/2 = 15. Therefore, the
image of a shot is a ci.
STEP 3
The only difference among the sample expressions is the color of the shot. The
variable for this difference is a-color.
STEP 4
The signature and purpose statement are
;; color → image
;; Purpose: Create a shot image of the given color
STEP 5
The function header is
(define (mk-shot-ci a-color)
STEP 6
The tests are below.
Observe that the last two sample computations and sample values tests are
property-based testing. They illustrate that the image returned by mk-shot-img
is a ci.
STEP 7
The body of the function is
STEP 8
All tests pass.
24 Alien Image
The alien image in Fig. 37 is the composition of two images of the same color: an
X and a circle. Specifically, the circle is overlayed over the X. A function to achieve
this uses function composition. The steps of the design recipe may be satisfied as
follows:
STEP 1
The alien image is computed using function composition. The outputs of text
and circle are inputs to overlay. The circle image is overlayed over the X
image. The alien image must be a ci.
STEP 2
Definitions for the value of sample expressions are
STEP 3
The only difference among the sample expressions is the color of the alien. The
variable for this difference is a-color.
STEP 4
The signature and purpose statement are
;; color → image
;; Purpose: Create an alien image of the given color
STEP 5
The function header is
STEP 6
The tests are below.
STEP 7
The body of the function is
STEP 8
All tests pass.
82 4 Aliens Attack Version 0
25 Rocket Image
The rocket image is the most complex of the images in Fig. 37. It is composed of the
images displayed in Fig. 40. To design a function to create a rocket image, a divide
and conquer bottom-up strategy is followed. That is, the simplest components are
independently designed first. Then simple components are combined to create more
complex figures.
It is important to keep in mind that creating images for the components of a larger
image requires experimentation. Rarely, if ever, are the dimensions of a component
right after the first attempt. This is another reason why it is important to write sample
expressions. Sample expressions may be repeatedly refined until the desired visual
effect is achieved. The steps of the design recipe presented for each component
below are the final result after several refinements improving the dimensions of each
component. Do not be afraid to experiment. Experimenting with sample expressions
may lead to insights on how to solve a problem.
Creating a rocket window image is a simple exercise with which to practice the steps
the design recipe.
STEP 1
A rocket window is a solid vertical oval.
STEP 2
Definitions for the value of sample expressions are
25 Rocket Image 83
STEP 3
The only difference among the sample expressions is the color of the oval. The
variable for this difference is a-color.
STEP 4
The signature and purpose statement are
;; color → image
;; Purpose: Create rocket window image
STEP 5
The function header is
STEP 6
Testing may be satisfied as follows:
STEP 7
The body of the function is
STEP 8
All tests pass.
84 4 Aliens Attack Version 0
STEP 1
The fuselage is a solid circle.
STEP 2
Definitions for the value of sample expressions are
STEP 3
The only difference among the sample expressions is the color. The variable for
this difference is a-color.
25 Rocket Image 85
STEP 4
The signature and purpose statement are
;; color → image
;; Purpose: Create the fuselage image
STEP 5
The function header is
(define (mk-fuselage-img a-color)
STEP 6
Testing may be satisfied as follows:
STEP 7
The body of the function is
STEP 8
All tests pass.
The single booster image is an equilateral triangle pointing down. Computing this
image requires function composition. The output of triangle is input to rotate.
STEP 1
A rocket single booster is an equilateral triangle image rotated 180◦ .
STEP 2
Definitions for the value of sample expressions are
86 4 Aliens Attack Version 0
Observe that in Fig. 40, the single booster image is the same color as the nacelle
image. Therefore, constants NACELLE-COLOR and NACELLE2-COLOR are defined
and used to write sample single booster (and nacelle) expressions.
STEP 3
The only difference among the sample expressions is the color. The variable for
this difference is a-color.
STEP 4
The signature and purpose statement are
;; color → image
;; Purpose: Create single booster image
STEP 5
The function header is
(define (mk-single-booster-img a-color)
STEP 6
Testing may be satisfied as follows:
STEP 7
The body of the function is
STEP 8
All tests pass.
25 Rocket Image 87
Observe that the booster image in Fig. 40d is two copies of a single booster image
side by side. This suggests that a constructor for a booster image needs a single
booster image as input.
STEP 1
A booster image is two copies of a single booster image side by side.
STEP 2
Definitions for the value of sample expressions are
STEP 3
The only difference among the sample expressions is the single booster image.
The variable for this difference is a-sb-img.
STEP 4
The signature and purpose statement are
;; image → image
;; Purpose: Create booster image
STEP 5
The function header is
(define (mk-booster-img a-sb-img)
STEP 6
Testing may be satisfied as follows:
STEP 7
The body of the function is
STEP 8
All tests pass.
Constructing an image for the rocket’s main body, as displayed in Fig. 40f, requires
three images: a window image, a fuselage image, and a booster image. It also requires
the use of function composition given that the fuselage goes above the booster and
the window goes on the fuselage.
STEP 1
The rocket’s main body is created by placing the fuselage above the booster and
then placing the window one quarter of the way down the height and half away
across the width of the fuselage.
STEP 2
Definitions for the value of sample expressions are
Observe that previously defined sample images are used to write the sample
expressions.
STEP 3
There are 3 differences among the sample expressions: the window, the fuselage,
and the booster images. The variables for these differences are, respectively,
a-window, a-fuselage, and a-booster.
STEP 4
The signature and purpose statement are
STEP 5
The function header is
(define (mk-rocket-main-img a-window a-fuselage a-booster)
STEP 6
Testing may be satisfied as follows:
(check-expect (mk-rocket-main-img
(mk-window-img 'green)
(mk-fuselage-img 'skyblue)
(mk-booster-img
(mk-single-booster-img 'lightred)))
STEP 7
The body of the function is
(place-image a-window
(/ (image-width a-fuselage) 2)
(/ (image-height a-fuselage) 4)
(above a-fuselage a-booster))
STEP 8
All tests pass.
90 4 Aliens Attack Version 0
The image of the rocket’s nacelle in Fig. 40e is deceptively simple. At first glance,
it looks like an ordinary rectangle. A closer examination of the rocket image, as
displayed in Fig. 41, reveals that the nacelle and the booster are the same color. In
addition, the nacelle must be the same width as the rocket’s main body and about a
quarter of the height of the main body. With these observations, the design of the
constructor for nacelle images follows.
STEP 1
The image of a nacelle is a rectangle that is the same width as the rocket’s main
body and a quarter of the height of the rocket’s main body height. The color of
the nacelle is the same color as the booster in the rocket’s main body.
STEP 2
Definitions for the value of sample expressions are
Observe that the same constants used to construct single booster images are used
to construct the corresponding nacelle images.
STEP 3
There are two differences among the sample expressions: the main rocket image
and the color of the nacelle. The variables for these differences are, respectively,
a-rocket-main-img and a-color.
STEP 4
The signature and purpose statement are
STEP 5
The function header is
(define (mk-nacelle-img a-rocket-main-img a-color)
STEP 6
Testing may be satisfied as follows:
STEP 7
The body of the function is
STEP 8
All tests pass.
92 4 Aliens Attack Version 0
The final image constructor needed is the rocket image constructor. This constructor
must place the nacelle over the rocket’s main body.
STEP 1
The rocket image is constructed by placing the nacelle image half the way across
the rocket’s main body and 30% from the bottom of the rocket’s main body.
STEP 2
Definitions for the value of sample expressions are
Notice that 30% from the bottom is the same as 70% from the top of the rocket’s
main body image. Since y values grow downward in the graphic plane, 70% of
the rocket’s main body image is computed.
STEP 3
There are two differences among the sample expressions: the rocket’s main body
image and the rocket’s nacelle image. The variables for these differences are
a-rocket-main-img and a-nacelle-img.
STEP 4
The signature and purpose statement are
;; image image → ci
;; Purpose: Create a rocket ci
Observe that the signature states that this function returns a ci. This is needed
because the returned image is intended for use in Aliens Attack.
STEP 5
The function header is
(define (mk-rocket-ci a-rocket-main-img a-nacelle-img)
STEP 6
Testing may be satisfied as follows:
25 Rocket Image 93
Observe that the output of this function must be a character image and, therefore,
ci? is used to test its properties. Furthermore, observe that there is a single
sample value test and that this test contains a significant amount of repetition.
Both of these are considered poor style. Testing ought to be more thorough. The
repetitions make it difficult to read and understand the test.
STEP 7
The body of the function is
(place-image a-nacelle-img
(/ (image-width a-rocket-main-img) 2)
(* 0.7 (image-height a-rocket-main-img))
a-rocket-main-img)
94 4 Aliens Attack Version 0
STEP 8
All tests pass.
** Ex. 51 — Refine the tests using sample values for mk-rocket-ci. Make
sure to eliminate repeated expressions and include more than one test.
*** Ex. 52 — Personalize your game with cis for the shot, the alien, and the
rocket of your own creation.
26 Drawing Functions
STEP 5
The function header is
(define (draw-ci char-img an-img-x an-img-y scn)
STEP 6
Testing may be satisfied as follows:
IMAGE-HEIGHT/2
STEP 7
The body of the function is
(place-image char-img
(image-x->pix-x an-img-x)
(image-y->pix-y an-img-y)
scn)
STEP 8
All tests pass.
The design now focuses on the two auxiliary functions to map image coordinates
to pixel coordinates. Let us first consider mapping the image coordinate pair (0, 0) to
26 Drawing Functions 97
a pixel coordinate pair. This coordinate pair is the top-left box in Fig. 38b. The center
of the image ought to be placed at the center of the box. The dimensions of the box
are IMAGE-WIDTH × IMAGE-HEIGHT. As illustrated in Fig. 42, this means that the
center of this box is at pixel coordinates (IMAGE-WIDTH/2, IMAGE-HEIGHT/2).
What is the pixel coordinate of image coordinate (3, 2)? This pixel coordinate
(IMAGE-WIDTH/2, IMAGE-HEIGHT/2) must be translated 3 boxes to the right and
2 boxes down. Therefore, the pixel coordinate for the box at the image coordinate (3,
2) is:
(3 * IMAGE-WIDTH + IMAGE-WIDTH/2,
2 * IMAGE-HEIGHT + IMAGE-HEIGHT/2)
In general, the image coordinate (x, y) maps to the pixel coordinate:
(x * IMAGE-WIDTH + IMAGE-WIDTH/2,
y * IMAGE-HEIGHT + IMAGE-HEIGHT/2)
Observe that the mapping works for image coordinate pair (0, 0).
The above insight guides the design of the image-coordinate to pixel-coordinate
functions. The steps of the design recipe to transform an image-x may be satisfied
as follows:
STEP 1
For image-x, ix, the corresponding pixel-x is ix * IMAGE-WIDTH
+ IMAGE-WIDTH/2
STEP 2
Definitions for the value of sample expressions are
STEP 3
The only difference among the sample expressions is the image-x value. The
variable for this difference is ix.
STEP 4
The signature and purpose statement are
;; image-x → pixel-x
;; Purpose: To translate the given image-x to a pixel-x
STEP 5
The function header is
(define (image-x->pix-x ix)
98 4 Aliens Attack Version 0
STEP 6
Observe that the tests using sample values are written for extrema values. That is,
the minimum and maximum possible values of an image-x are used.
STEP 7
The body of the function is
STEP 8
All tests pass.
The design of image-x->pix-x does not reveal the need for any new auxiliary
functions. The only task left is to design a function to translate an image-y to a
pixel-y. The steps of the design recipe may be satisfied as follows:
STEP 1
For image-y, iy, the corresponding pixel-y is iy * IMAGE-HEIGHT +
IMAGE-HEIGHT/2
STEP 2
Definitions for the value of sample expressions are
STEP 3
The only difference among the sample expressions is the image-y value. The
variable for this difference is iy.
STEP 4
The signature and purpose statement are
;; image-y → pixel-y
;; Purpose: To translate the given image-y to a pixel-y
26 Drawing Functions 99
STEP 5
STEP 6
Once again, observe that the tests using sample values are written for extrema
values.
STEP 7
The body of the function is
STEP 8
All tests pass.
The design of image-y->pix-y does not reveal the need for any new auxiliary
functions. This means that all the functions needed to draw a ci have been designed
and implemented.
Making decisions is part of life. For example, while driving on a highway, the driver
monitors the car’s speed. If the speed is too fast, then the driver decreases the speed.
If the speed is too slow, then the driver increases the speed. Otherwise, the driver
does not change the speed. It should not come as a surprise that making decisions is
also part of problem solving. Modern cars are equipped with a cruise control system.
The driver sets the cruise control at a certain speed. If the speed is too fast, then the
cruise control program decreases the speed. If the speed is too slow, then the cruise
control program increases the speed. Otherwise, the cruise control program does not
change the speed.
Regardless of whether it is a driver or a cruise control program, the decision
on how to adjust the speed depends on the car’s current speed. This decision
is may be represented by one of three possible expressions, say, 'increase,
'decrease, or 'steady. Think carefully about what this means. Multiple ex-
pressions for the value of a function means that determining how to adjust the
speed is a compound function. We can, in fact, write a mathematical func-
tion:
⎧
⎪
⎨'decrease if too-fast?(a-speed)
speed-change(a-speed) = 'increase if too-slow?(a-speed)
⎪
⎩
'steady otherwise
Two predicates, too-fast? and too-slow?, are used to evaluate the given speed.
In essence, the above function is stating that there is data variety in the domain of the
function. The input may be in one of three speed intervals indicating that the car is
either traveling too fast, too slow, or neither. In order to determine the speed change
a decision must be made as to which of the 3 expressions needs to be evaluated. This
decision is based on the speed range a-speed belongs to.
It turns out that speed ranges may be used to solve many problems. For example,
a police officer may use the following function to determine a course of action after
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 101
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_5
102 5 Making Decisions
Observe that this function must also decide which expression to use to provide an
answer for a given speed. Furthermore, observe that the two functions have the same
basic structure. This structural similarity stems from the fact that they process the
same type of data and suggests that a function template may be developed to process
a speed. A function template is like a skeleton for functions. It captures the expected
similarities among function that process the same type of data. For each data type a
problem solver defines a function template is developed.
For example, speed may be defined as follows:
A speed is a number such that either:
1. too-fast?(a-speed) is true
2. too-slow?(a-speed) is true
3. Neither too-fast?(a-speed) nor too-slow?(a-speed) is true
the template for functions that process speed is:
⎧
⎪
⎨. . . if too-fast?(a-speed)
f-on-speed(a-speed) = . . . if too-slow?(a-speed)
⎪
⎩
. . . otherwise
This template may be used to design any function that processes a speed. By providing
the missing expressions and a function name both functions above are obtained.
We can surmise from the examples above that compound functions arise from data
having variety. These functions need to decide which expression to evaluate based
on the variety the input belongs to. All functions that process the same data type with
variety have the same structure. This chapter introduces how to design functions that
make decisions. It introduces new BSL syntax to write conditional expressions which
are used to make decisions. A design recipe for compound functions is presented that
includes the development of data definitions for data with variety and of function
templates to capture structural similarities among functions. Several different types
of data with variety are explored.
In BSL, there is syntax for expressions to make decisions. These new expressions are
called conditional expressions. The new expr production rule is:
This rule states that a conditional expression starts with an opening parenthesis
and the keyword cond. Afterwards, there is 1 or more stanzas delimited by square
brackets. Each stanza contains two expressions. The first expression is a condition
and must evaluate to a Boolean. Think of this expression as a question whose
answer is either true or false. The second expression is called the consequence
and is only evaluated if the corresponding condition evaluates to #true. Think of
this expression as the value of the conditional expression when the answer to the
corresponding question is true. The last stanza in a conditional is called the default
stanza and contains in square brackets the keyword else and a single expression. This
expression represents the default value of the conditional. The default expression is
only evaluated if the conditions in all the other stanzas evaluate to #false.
The mathematical function speed-change(a-speed) is translated into BSL
syntax as follows:
(define (speed-change-bsl a-speed)
(cond [(too-fast? a-speed) 'decrease]
[(too-slow? a-speed) 'increase]
[else 'steady]))
The stanzas of a conditional are evaluated from top to bottom. For each stanza the
condition is evaluated first. If the condition evaluates to #true then the value of the
consequence is the value of the conditional. If the condition evaluates to #false
then evaluation continues with the next stanza. If the default stanza is reached, then
the default expression is evaluated to obtain the value of the conditional expression.
For example, for speed-change-bsl, if too-fast? evaluates to #true then the
value of the conditional is 'decrease. If it evaluates to #false then evaluation
continues with the next stanza. If too-slow? evaluates to #true then the value of
the conditional is 'decrease. If it evaluates to #false then evaluation continues
with the next stanza. The next stanza is the else stanza and, therefore, the default
expression is evaluated making the value of the conditional 'steady.
BSL provides shorthand when there are only two varieties of data. That is, short-
hand may be used when only the default stanza and one other stanza is needed. The
shorthand syntax is called an if-expression:
This rule states that an if-expression is formed by an open parenthesis, three ex-
pressions, and a closing parenthesis. The first expression is called the condition and
must evaluate to a Boolean. If the condition is #true then the second expression is
evaluated to obtain the value of the if-expression. Otherwise, the third expression
is evaluated to obtain the value of the if-expression.
Consider, for example, translating the absolute value mathematical function into
BSL. In mathematical syntax the function is defined as follows:
a-num if a-num >= 0
abs-value(a-num) =
-a-num otherwise
104 5 Making Decisions
Observe that there are only two varieties of numbers for this function: nonnegative
and negative. Only two varieties means that an if-expression may be used to translate
it to BSL syntax:
(define (abs-value-bsl a-num)
(if (>= a-num 0)
a-num
(* -1 a-num)))
If the condition, (>= a-num 0), evaluates to #true then the value of the if-
expression is a-num. If the condition evaluates to #false then the value of the
if-expression is (* -1 a-num).
** Ex. 55 — Write a program to decide the speed change for a car if the
maximum speed is 120 kilometers per hour and the minimum speed is 50
kilometers per hour.
When problem analysis reveals that data with variety needs to be processed then a
data definition must be created. Given that there is variety in the data, any function
that processes this type of data requires a conditional expression in its body. A
conditional expression is needed to determine which of the data varieties is being
processed. For each variety an expression for the value of the function needs to be
developed. A problem solver must also develop a function template to capture the
structural similarities among all functions that process the defined data type. Finally,
the number of tests must be greater than or equal to the number of varieties in the
data. In addition to at least one test per variety, it is also good practice to test border
cases that delimit varieties.
Let us examine poem verses as an example. First, we must choose how to rep-
resent a verse. We may, for example, represent a verse as a string. Some poets
feel that it is good practice to limit the length of each verse to 35 characters. A
verse with more than 35 characters is considered too long. A verse with 15 to 35
characters is considered fine. A verse with less than 15 characters is considered
too short. Based on this description, a data definition for a verse is formulated as
29 Designing Functions to Process Data with Variety 105
follows:
;; A verse is either:
;; 1. A string of length greater than 35
;; 2. A string of length 15 to 35
;; 3. A string of length less than 15
The data definition for a verse informs problem solvers that any function that pro-
cesses a verse must have a conditional in its body that determines the variety of the
input verse. In addition, it informs problem solvers that at least 3 tests are needed:
one for each variety. Remember, we say at least because it is always good practice
to test boundary values. In this case, 5 tests are reasonable: one for each variety,
one for length 35, and one for length 15. It is important to note that verse vari-
eties are mutually exclusive. This means that no verse can be part of more than one
variety.
Based on the above data definition, observe that all functions that process a verse
share these features unique to verses:
1. A signature that has at least a verse as an input type.
2. A conditional in the body that determines if the given verse’s length is greater
than 35, between 15 and 34, or less than 15.
3. Sample expressions, if any, for each variety that is used to compute a value.
4. One test for each sample expression
5. Sample value tests including tests for borderline cases of length 35 and 15.
106 5 Making Decisions
This leads to the function template in Fig. 43. Observe that the signature includes a
verse as input and that each stanza in the skeleton’s body has a Boolean expression to
determine the variety of the verse. The default stanza, of course, does not explicitly
state that its Boolean expression is (< (string-length a-verse) 15). This
is unnecessary because verse varieties are mutually exclusive. Mutual exclusion
guarantees that if the first two Boolean expressions are false then the verse’s length
is less than 15. Stated differently:
¬(> (string-length a-verse) 35) ∧
¬(<= 15 (string-length a-verse) 35)
⇒
(< (string-length a-verse) 15)
This logical expression is stating that if the length of a verse is not greater than 35 and
it is not between 15 and 35 then it is less than 15. Finally, observe that the template
suggests 3 sample expressions and sample computation tests (one for each variety)
and 2 sample computation tests (one for each boundary value). The important point
to remember that there must be at least one test for each variety. More tests, of course,
may be added in the interest of clarity.
This function template can be specialized to express the solution to any problem
that requires processing a verse. Consider the problem of computing a string to
describe the length of a verse. To solve the problem, the function must decide
which constant string describing its variety to return: "Too Long", "Fine", or
"Too Short". Given that a verse is not processed to create any of these constant
strings, there are no sample expressions to describe how a verse is used to compute
these strings. If there are no sample expressions, there are also no tests using sample
computations.
The function’s signature, purpose, and header are:
;; verse → string
;; Purpose: Determine if the given verse is too long, fine,
;; or too short
(define (verse-type a-verse)
Observe that the signature uses the type, verse, defined by the data definition above.
Once a type is defined by a data definition it may be used in signatures. Signatures,
therefore, may contain types defined by BSL or by a data definition the programmer
creates. Further observe that the signature only has one input and, therefore, the
function only has one parameter. Finally, observe that the name of the function is
suggestive of its purpose and the name of the parameter is suggestive of the value it
represents.
Given that there are no sample expressions to test (as noted above), the tests using
sample values must be specialized to include a test for each variety of verse and for
each borderline length. Sample tests are:
;; Tests using sample values
(check-expect
(verse-type "Here is a sigh to those who love me,")
"Too Long")
29 Designing Functions to Process Data with Variety 107
(check-expect
(verse-type "And a smile to those who hate")
"Fine")
(check-expect (verse-type "Sorry for fate") "Too Short")
(check-expect
(verse-type "The Battle of Culloden started now,")
"Fine") ;; verse length 35
(check-expect
(verse-type? "Flaming heavens")
"Fine") ;; verse length 15
108 5 Making Decisions
The next step is to specialize the body of the function in the template. For each
verse variety the proper string needs to be returned. The specialized body is:
(cond [(> (string-length a-verse) 35) "Too Long"]
[(<= 15 (string-length a-verse) 35) "Fine"]
[else "Too Short"]))
The complete program is displayed in Fig. 44. To make Fig. 44 easier to read the
comments including the data definition and the function template are displayed using
an italic font.
The development of verse-type suggests a series of steps that guide the de-
velopment of a decision-making function. Figure 45 displays the design recipe
for decision-making functions. This design recipe is a refinement of the general
design recipe for functions in Fig. 28. Therefore, the steps ought to feel familiar.
Step 1 requires the development of data definitions. Of these, at least one must
have varieties that are mutually exclusive. Step 2 requires the development of a
function template. The function’s body must be a conditional that has a stanza for
each variety. The Boolean expressions to determine the variety of the input are
part of the function’s body in the template. In addition, there must be at least one
test outlined for each data variety. Commonly (but not always), sample expression
tests are used to test each variety and sample value tests are used to test boundary
values. These first two steps are part of what is called data analysis and do not
have to be repeated for every problem being solved using the data type defined.
That is, these steps are performed once and the template may be used multiple
times.
The rest of the steps are taken to solve a specific problem. These are the steps
that need to be repeated for each problem solved. These steps specialize the func-
tion template. Step 3 requires an outline of how the function’s value is computed.
Step 4 requires the specialization of constant definitions for the values of sample
expressions. The sample expression illustrates how an answer is computed by pro-
cessing a given variety. The differences are named and serve as parameters to a
function definition. If a variety is not processed to formulate an answer, then no
sample expression is written. Steps 5 and 6 specialize the signature, the purpose
statement, and the function header. Step 7 requires the specialization of tests. There
must be at least one test for each data variety. Tests ought to also be thorough and,
29 Designing Functions to Process Data with Variety 109
therefore, test boundary values between varieties. Write tests that use the constants
defined for sample computations and tests that use sample values. Step 8 requires the
specialization of the function’s body. Fill in the expressions for the answer of each
variety one at a time. Step 9, as before, requires running the tests and redesigning if
necessary.
The design recipe for decision-making functions informs us that data variety plays
a central role in problem solving. For example, the number of sample expressions,
of stanzas in a conditional, and of tests is proportional to the number of varieties in
the data. That is, the shape of the data influences the shape of a solution/program.
This suggests the function templates displayed in Fig. 46. Figure 46a displays the
function template for data with more than two varieties. Figure 46b displays the
function template for data with two varieties. In both, the number of constants for
sample expressions, of tests, and of needed expressions in the body of the function
are equal to the number of varieties. It is, of course, reasonable to increase the
110 5 Making Decisions
number of constants and tests for more thorough testing or to add clarity to the
program.
To illustrate the steps of the design recipe we will explore several different types
of data that have variety. It is impossible to outline all imaginable types of data with
variety. The goal here is to illustrate how to solve problems with several common
data type patterns that have variety.
30 Enumeration Types
A data definition that lists all possible values is called an enumeration type. To a
programmer this means that the conditional in a function that processes this type of
data only needs to distinguish the cases listed in the data definition. This is done by
checking for equality with the values listed in the data definition. Although this data
type represents the simplest form of variety, it may be cumbersome to use as the
number of varieties grows.
To illustrate the design process using an enumerated type consider creating
an animation for of a traffic light. We design our program for a simplified traf-
fic light that only changes colors from green to yellow to red to green and so
on. That is, the possibilities of a flashing red or a flashing yellow are not in-
cluded the design. Based on this there are only three possible traffic light images,
which may be defined as constants. Each image is created by overlaying the three
lights over a background. For example, the three images may be defined as fol-
lows:
(define BACKGROUND (rectangle 60 180 'solid 'black))
Observe that if the traffic light is increased by 1 (remainder 3) the next light
is the correct one. For example, increasing from 0 to 1 (remainder 3) means
that the traffic light changes from green to yellow. Similarly, incrementing
2 (remainder 3) results in 0 meaning that the traffic changes from red to
green.
STEP 2
The function template for tick is:
;; tick ... → ...
;; Purpose: ...
(define (f-on-tick a-tick) ...)
There is no variety in the data definition for tick. Therefore, f-on-tick does not
have a conditional. No decision needs to be made. The number of tests is also
arbitrary as there are no specific values that must be tested.
On the other hand, the data definition for tl has variety. Observe that there
is a conditional in the body of f-on-tl. There is a stanza for each vari-
ety of tl. The tests for each variety are defined here. The template also sug-
gests writing a sample expression and a corresponding test for each variety of
tl.
Now that the steps for data analysis have been completed, we can start solving
the problem of creating functions for the traffic light animation. We know that a
function to process ticks is needed. This function must return an image of a traffic
light. Starting with Step 3, the following the design recipe yields:
STEP 3
Given a tick, two values need to be computed. From a tick a tl value must be
computed. From a tl value an image must be computed. The computation of these
values is deferred to yet unwritten functions. The function tick->tl converts a
tick to a tl. The function image-of-tl converts a tl into a traffic light image.
STEP 4
To specialize the sample expressions, pick three different tick values such that
each maps to a different traffic light image. Specializing tests in this manner
guarantees to illustrate how a clock tick is mapped to each of the possible images.
Here are some sample expressions:
;; Sample expressions for draw-tick
(define TICK12-IMG (image-of-tl (tick->tl 12)))
(define TICK25-IMG (image-of-tl (tick->tl 25)))
(define TICK32-IMG (image-of-tl (tick->tl 32)))
The only difference among the three sample expressions is the value for tick. The
name for this difference is a-tick.
STEP 5
The signature and purpose statement are specialized as:
;; tick → image
;; Purpose: Compute the traffic light image for the
;; given tick
114 5 Making Decisions
STEP 6
The function header is specialized to:
(define (draw-tick a-tick)
STEP 7
The tests are specialized as follows:
The sample value tests illustrate that the function works for more than three
predefined computations.
STEP 8
The body of the function is specialized as follows:
(image-of-tl (tick->tl a-tick))
STEP 9
All tests pass.
The result of Step 9 is expected for now. For the design to be complete all tests
must pass after the auxiliary functions are designed and implemented.
Top-down design has revealed that two auxiliary functions, tick->tl and
image-of-tl, are needed. Each is designed independently of the other. It does
not matter which is designed first. Let us start with tick->tl. Here is an example
of how to satisfy the steps of the design recipe:
STEP 3
Given a tick, the corresponding tl is given by the remainder of the tick and 3.
STEP 4
These are specialized sample expressions to illustrate how each variety of tl is
computed from a tick value:
The only difference among the three sample expressions is the value for tick. The
variable for this difference is a-tick.
STEP 5
The specialized signature and purpose statement are:
;; tick → tl
;; Purpose: Convert the given tick to a tl
STEP 6
The specialized function header is:
(define (tick->tl a-tick)
STEP 7
The specialized tests are:
STEP 8
The body of the function is:
(remainder a-tick 3)
STEP 9
All tests pass.
The design of tick->tl did not reveal the need for any new auxiliary functions.
Therefore, the only remaining task is the design of image-of-tl. For this function,
the presentation of the design is changed. Instead of outlining the result each step of
the design recipe one at a time, the changes made to the template for functions on tl
is directly presented.
This function converts a given tl to an image. We also observe that a tl is only
used to decide which image to return. Since a tl is not used to compute a value there
are no sample expressions or tests using sample expressions to write. Based on these
observations, the initial template specialization looks as follows:
116 5 Making Decisions
;; tl → image
;; Purpose: Return the traffic light image for the given tl
(define (image-of-tl a-tl . . .)
(cond [(= a-tl 0) . . .]
[(= a-tl 1) . . .]
[else ...]))
The final step to complete the traffic light simulation program is to require the
universe teachpack and to write the animate expression. Figure 47 displays the
structure of the program. Elements in angle brackets have been omitted from the
figure due to space limitations. It should be clear, however, what needs to be filled in
from all the design steps illustrated above. The second line in the program requires
the universe teachpack. The last line in the program is telling DrRacket to animate
the program using the draw-tick function. Congratulations! You have written your
first animation. Run the program. What do you think?
;; tick tl
;; Purpose: Convert the given tick to a tl
(define (tick->tl a-tick) (remainder a-tick 3))
;; tl image
;; Purpose: To return the traffic light image for the given tl
(define (image-of-tl a-tl)
(cond [(= a-tl 0) G-ON]
[(= a-tl 1) Y-ON]
[else R-ON]))
;; tick image
;; Purpose: Compute the traffic light image for the given tick
(define (draw-tick a-tick)
(image-of-tl (tick->tl a-tick)))
(animate draw-tick)
31 Interval Types
Running the traffic light simulation reveals that the light changes too fast–28 times
per second. The program is well-designed and works from a certain perspective but
needs to be refined. That is, it needs to be improved. This is our first dive into the
process of iterative refinement. It is fairly clear that we need the light to change at a
slower pace so that the human eye can appreciate the changes. Instead of changing
the image 28 times per second, the animation may change the traffic light image, for
example, twice per second. This means that, instead of changing every clock tick,
the image needs to change every 14 clock ticks.
This refinement means that the data definition of tl must be updated. Instead of
defining tl as 0, 1, or 2, tl could define as follows:
;; A traffic light (tl) is either
;; 1. 0 --means the green light is on
;; 2. 1 --means the green light is on
..
.
;; 13. 13 --means the green light is on
;; 14. 14 --means the yellow light is on
..
.
;; 27. 27 --means the yellow light is on
;; 28. 41 --means the red light is on
..
.
;; 41. 41 --means the red light is on
This data definition leads to a function definition whose conditional has 42 stan-
zas. That is quite large and error-prone during development. Sometimes such data
definitions are unavoidable. Observe, however, that there is something special about
this (long) data definition. There is a lot of repetition. This suggests that there is an
abstraction that needs to be defined.
Many consecutive numbers have the same meaning. For example, the values from
0 through 13 all mean that the green light is on. We can borrow an idea from high
school mathematics called an interval. We can say, for example, that when tl is a
member of [0..13] it means that the green light is on. Equivalently, we can say that tl
is a member of [0..14), (-1..13], or (-1..14). Remember that a square bracket means
included and a parenthesis means not included. This leads to the following data
definition:
;; A traffic light (tl) is a member of either
;; 1. [0..13] --means the green light is on
;; 2. [14..27] --means the yellow light is on
;; 3. [28..41] --means the red light is on
This is called an interval type. An interval type defines orderable data by a set of
categories. For example, tl is defined using three intervals. The intervals must be
31 Interval Types 119
STEP 5:
;; tick --> tl
;; Purpose: Convert the given tick to a tl |#
;; STEPS 6 and 8
(define (tick->tl a-tick)
(remainder a-tick 42))
;; STEP 7: TESTS
;;Tests using sample expressions
(check-expect (tick->tl 11) TL-0)
(check-expect (tick->tl 60) TL-1)
(check-expect (tick->tl 123) TL-2)
mutually exclusive. That is, an instance of the data type must be a member of only
one interval.
Refining a data definition means that the program must also be refined. Any
function that manipulates or creates an instance of the refined data type and its
tests must be updated. To determine which functions to update, a programmer must
look for expressions that manipulate or create an instance of the updated data type.
Functions that are good candidates to be updated are those with signatures that refer
to the refined data type. Examining the signatures in the traffic light simulation
program reveals:
tick->tl: tick → tl Needs to be updated, because it creates an instance of tl.
image-of-tl: tl → image Needs to be updated, because it makes a decision based
on an instance of tl.
draw-tick: tick → image Doest not need to be updated. This function does not
manipulate nor creates an instance of tl.
The refinements necessary involve updating expressions and tests. The tick->tl
function must convert a tick into a tl. Finding the remainder of the given tick by 3
is no longer correct because a tick must be converted to an integer in [0..41].
Modular arithmetic, however, can still be used. Now, the remainder of a tick by 42
must be computed. Figure 48 displays the updated tick->tl function. The sample
120 5 Making Decisions
STEP 5:
;; tl --> image
;; Purpose: To return the traffic light image for the given tl
|#
;; STEPS 6 and 8
(define (image-of-tl a-tl)
(cond [(<= 0 a-tl 13) G-ON]
[(<= 14 a-tl 27) Y-ON]
[else R-ON]))
expressions and the body of the function now use 42 instead of 3. The tests have
been updated in order to have at least one test for each variety of tl using the new
data definition.
Figure 49 displays the refined version of image-of-tl. The image-of-tl func-
tion processes an instance of tl. This means that the conditional expression must
be refined to correspond to the new data definition of tl. Observe that the tests in-
side the conditional now determine what interval contains a-tl. The unit tests for
image-of-tl are also refined to include at least one test for each interval of tl.
An important lesson to absorb is that data in the real or an imaginary world may
be represented in different ways (like tl). Part of the design process is to select a
representation. The representation chosen influences how a problem solver thinks
about a program. Exploring different representation may provide insight into the
problem. It is also important to keep in mind that how data is represented may have
a profound impact on performance.
* Ex. 65 — For the traffic light simulation three images are defined: G-ON,
Y-ON, and R-ON. Rewrite the traffic light simulation using the following data
definition:
;; A traffic light (tl) is either
;; 1. G-ON --means the green light is on
;; 2. Y-ON --means the yellow light is on
;; 3. R-ON --means the red light is on
*** Ex. 67 — Design and implement an animation for a pedestrian traffic light.
A pedestrian traffic light displays one of two messages: Walk or Don’t Walk.
The Walk message should be displayed for 4 s before it changes. The Don’t Walk
message should be displayed for 2 s before it changes.
32 Itemization Types
Enumeration types capture variety by exhaustively listing all possible values. Interval
types capture variety using a set of intervals to classify orderable data. What is
done if both a listing of values and intervals are needed? As it turns out this is
sometimes needed. To define such data an itemization type is used. An instance of
an itemization type may be a specific value or a member of an interval type. Observe
that an itemization type is a generalization of enumeration and interval type. When
all varieties are specific values an itemization type is called an enumeration type.
When all varieties are intervals an itemization type is called an interval type.
To illustrate how to design a program using an itemization type consider the prob-
lem of doubling or rotating an image using single alphanumeric or arrow keystrokes.
The image is rotated 90, 180, or 270◦ using, respectively, the right, down, and left
arrow keys. The image is doubled using the up arrow key. Nothing is done with the
image on an alphanumeric keystroke.
Before proceeding with the design of the solution it is necessary to know how keys
are represented and compared. In the universe teachpack, all keys are represented
with a string. For example, the z key is represented by "z". The right, down, left, and
up keys are represented, respectively, with the strings "right", "down, "left", and
"up". The key=? function is a predicate used to compare keys for equality. Do not
122 5 Making Decisions
mistake key=? as being the same as string=?. The former only compares strings
that represent keys while the latter compares any two strings.
To facilitate the design, assume that there is a constant ROCKET-IMG whose value
is a rocket image created using the functions in the image teachpack. Figure 50
displays Step 1 of the design recipe. An alphanumeric keystroke is an interval type.
There are two intervals: one for letters and one for numbers. Nothing else is an
alphanumeric keystroke. A change image keystroke is an itemization type. It has
four literal values: one for each arrow. It also has one interval type: an alphanumeric
keystroke. Observe that a data definition may refer to another data definition. The
meaning of each variety is also clearly stated for each variety within each data
definition.
Figures 51 and 52 display Step 2 of the design recipe. In Fig. 51, the template for
ciks has a function skeleton with a conditional that distinguishes the variety of the
value received as input. The template also suggests defining a constant and a test for
the value of a sample expression for each variety of ciks. In addition, it suggests the
definition of a sample value test for each variety of ciks. In Fig. 52, the template for
aks has a function skeleton with a conditional that distinguishes between the varieties
of aks. Two sample expressions along with corresponding test are suggested: one
for a letter keystroke and one for a digit keystroke. In the same manner, two sample
value tests are suggested by the template.
Figure 53 displays Steps 3 through 8 of the design recipe for the function that
processes a ciks. Step 3 explains that the rotate and scale functions are used
to compute new images. Step 4 illustrates how these two functions are used to
compute a new image using the ROCKET-IMG constant. Step 5 results in the spe-
cialized signature and purpose statement. The signature states the function takes
two inputs: an image and a ciks. The purpose statement indicates that the given
image is rotated, doubled, or left untouched. The result of step 6 reflects the re-
quirements stated by the signature. The two parameters are named in a manner that
suggest the type of their value. The name of the function is suggestive of its pur-
pose. Step 7 results in tests using sample computations and using sample values.
The tests with sample computations illustrate that the function computes the same
value as the sample expressions. The tests with sample values use images created
32 Itemization Types 123
using functions from the image teachpack. The third test uses flip-vertical.
This test passes because vertically flipping an equilateral triangle results in the
124 5 Making Decisions
same image as rotating the triangle 180◦ . In general, however, this is not true.
For example, vertically flipping a right triangle does not result in the same image
as rotating the triangle 180◦ . Finally, Step 8 specializes the body of the function.
The expressions for the consequence in each stanza of the conditional are based
on the sample expressions and on the last test using a sample value. For the for-
mer, the literal ROCKET-IMG is substituted with, an-img, the image parameter of
the function. For the latter, observe that the input image is the same as the output
image. This means that in the default stanza the value of the parameter must be
returned.
The final noteworthy observation is that the design of change-img did not reveal
the need for auxiliary functions. This means that the template for aks was never used
to design a function. That is fine. Not every problem requires every template to be
specialized. Other problems involving ciks may very well require the specialization
33 What Have We Learned in This Chapter? 125
of the aks function template. Think of the templates developed as different tools in a
toolbox. Sometimes you only need a hammer. Other times you only need a wrench.
Yet other times you need both.
** Ex. 68 — Refine the change image program to flip the given image vertically
for a letter keystroke and to flip the given image horizontally for a number
keystroke. Hint: There is no need to change any data definitions for this problem.
We all know that video games are fun to play. Designing them can also be fun despite
being complex programs. To manage this complexity, video games are developed
using the process of iterative refinement. Adding features to a video game in a
piecemeal manner allows the problem solver to conquer the complexity in small
steps. For example, for Aliens Attack we have already solved the problem of creating
cis (remember, ci is defined as a character image in 4).
Before proceeding, it is a good idea to get a handle on exactly what a video
game is. In many ways, a video game is similar to an animation. For instance,
scenes are flickered fast enough to create the illusion of movement. Video games,
however, allow players to interact with the animation. This interaction may change
the evolution of the game. The evolution of the game changes when a computer
event occurs. A computer event includes any action like pressing a key, the clock
ticking, or clicking the mouse. Players change the evolution of the game through, for
example, key pressing or mouse actions. For instance, in Aliens Attack a player may
use the left arrow and the right arrow keys to move the rocket. Like a simulation, the
evolution of a video game may also change by events not controlled by the player
like a clock tick. In Aliens Attack, for example, the aliens move every time the clock
ticks. It is now clear that functions called event handlers (or simply handlers) are
needed to process computer events.
The ability to design programs that make decisions allows us to implement video
games. Handlers need to make decisions. For example, the handler to process a
keystroke must distinguish what key has been pressed. If the right arrow key is
pressed, then the handler moves the rocket right. If the left arrow key is pressed, then
the handler moves the rocket left. Clearly, data definitions are needed to describe the
keystroke events that change the evolution of the game.
In addition to providing programmers with the ability to write animations, the
universe teachpack provides programmers with the ability to write video games in
BSL. This teachpack is an API. It provides the necessary syntax to associate handlers
with computer events and it defines the signature that these handlers must have.
Using the universe teachpack is an exercise in programming with (the abilities
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 127
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_6
128 6 Aliens Attack Version 1
provided by and the restrictions imposed by) an API. Programming with an API is
a good skill to develop because programming with APIs is standard practice.
The universe teachpack requires the development of a data definition for a world.
A world is all the values that may change during the evolution of the game. Some of
the values may be de displayed when a game image is computed. Other values may
only be needed to advance the evolution of the game. In a fully implemented Aliens
Attack game, for example, the aliens and the direction the aliens travel in are part of
the world. The aliens are displayed in a game image as in Fig. 37. The direction the
aliens travel in, on the other hand, is not displayed in the image.
There is a special expression to run a video game called a big-bang expression
(bb-expr). This expression allows the programmer to provide the initial value of the
world and the handlers needed to advance the evolution of the game. The big-bang
expression syntax is:
bb-expr ::= (big-bang expr bb-clause+)
bb-clause ::= [to-draw function]
[on-key function]
[on-tick function]
[stop-when function]
[name expr]
..
.
A bb-expr has in parenthesis the keyword big-bang followed by an expression
that must evaluate to a world value followed by one or more bb-clauses (big-
bang clauses). A bb-clause associates an event with a handler function to pro-
cess the event. Each bb-clause has in square brackets an event describer (e.g.,
to-draw, on-tick, and stop-when) and a handler for the event. For example,
[to-draw draw-world] states to use draw-world to render the image of the
world, [on-key process-key] states to use process-key to process a key stroke,
[on-tick process-tick] states to use process-tick to process a clock tick,
and [stop-when game-over?] states to use game-over? to detect the end of
the game (or interactive simulation). The only required bb-clause is the to-draw
clause. The only clause that does not follow this scheme is the name clause. This
clause associates a name with the world. The expr in this clause must evaluate to a
string or symbol. The name appears in title of the scene. The vertical dots mean that
there are other varieties of bb-clauses. The big-bang documentation describes all
the possible bb-clauses. An important thing to remember is that a bb-clause may
not be repeated. For example, a bb-expr may only have one to-draw bb-clause.
The universe API also specifies the signature and purpose of the handlers. Each
handler serves a specific purpose like creating a scene, computing the next world
after a clock tick or keystroke, or detecting the end of the game/simulation. For
34 The Universe Teachpack 129
example, the following table displays the signature and purpose for four handlers:
on-draw: world → scene
Purpose: To create the world’s scene
on-key: world key → world
Purpose: To return the world after the given keystroke
on-tick: world → world
Purpose: To return the world after a clock tick
stop-when: world → Boolean
Purpose: To determine if the game/animation has ended
This means that we must write handlers that satisfy the above signatures. To a
programmer this suggest using a top-down design strategy to write a video game. Start
with the functions needed by the big-bang expression. Design auxiliary functions
as their need is revealed.
To illustrate the use of the universe teachpack consider developing an interactive
animation to rotate the image of the fish displayed in Fig. 55. The user may rotate
the image of the fish to point up, down, left, or right using the arrow keys. In Fig. 55
the fish is pointing up. If the user presses the up arrow key, then the image remains
unchanged. If the user presses the right arrow, then the image is rotated to make the
fish point right. A similar action takes place pressing the other arrow keys. If the
user presses any other key, then the fish image remains unchanged regardless of the
direction the fish is pointing in. For example, if the fish is pointing left and the m key
is pressed, then the fish image remains unchanged.
To start, first define the constants that are needed. A scene is needed. A scene is
defined as follows:
A scene is a WIDTH x HEIGHT image
Here WIDTH and HEIGHT are positive integer constants. Presumably, a scene is large
enough to contain the fish image. Otherwise, the fish image is truncated.
The fish image is also a constant. The following code defines all the scene and
fish image constants:
(define WIDTH 200)
(define HEIGHT 200)
(define E-SCENE-COLOR 'black)
(define E-SCENE (empty-scene WIDTH HEIGHT E-SCENE-COLOR))
130 6 Aliens Attack Version 1
The body of run gives us a starting point for the design process. The initial world,
INIT-WORLD, needs to be defined. For this, a world data definition is needed. The
function handlers draw-world and process-key and any needed auxiliary func-
tions need to be designed. Recall that the signatures of the handlers are defined by
the universe API (noted as comments in run). For the key-processing handler, a
data definition is needed for the keys that affect the evolution of the animation.
We start with the data definition for the keys that affect the evolution of the anima-
tion. Remember that every data definition means the development of a corresponding
function template. A fish keystroke is defined as follows:
A fish keystroke (fks) is either:
1. "up" --means fish in up direction
2. "right" --means fish in right direction
3. "down" --means fish in down direction
4. "left" --means fish in left direction
;; fks . . . → . . .
;; Purpose: . . .
(define (f-on-fks a-fks . . .)
(cond [(string=? a-key "up") . . .]
[(string=? a-key "right") . . .]
[(string=? a-key "down") . . .]
[else . . .]))
A key is either:
1. fks
2. not an fks
;; key . . . → . . .
;; Purpose: . . .
(define (f-on-key a-key . . .)
(if (fks? a-key)
...
. . .))
;; world . . . → . . .
;; Purpose: . . .
(define (f-on-world a-world)
. . .(f-on-fks a-world). . .)
and that all possible returned fish images are produced by these. Therefore, there
is no need for tests using sample values. Based on these observations, the final
specialization step yields:
;; fks → image
;; Purpose: Compute the fish image for the given fks
(define (compute-fish-img a-fks)
(cond [(string=? a-key "up") FISH-IMG]
[(string=? a-key "right") (rotate 270 FISH-IMG)]
[(string=? a-key "down") (rotate 180 FISH-IMG)]
[else (flip-vertical (rotate 90 FISH-IMG))]))
Once again, observe that template specialization completes all the steps of the
design recipe except running the tests.
The function process-key, as its name suggests, processes a key. This means
the template for functions on a key must be specialized. This function always takes
as input a world and a key and returns a world according to the universe API. If
the given key is an fks, then the next world is the given key. Otherwise, the world is
unchanged. The initial specialization yields:
;; world key → world
;; Purpose: To return the next world based on the
;; given key
(define (process-key a-world a-key . . .)
(if (fks? a-key)
...
. . .))
;; key → Boolean
;; Purpose: Determine if the given key is an fks
(define (fks? a-key)
(if (or (key=? a-key "right")
(key=? a-key "down")
(key=? a-key "left")
(key=? a-key "up"))
#true
#false))
** Ex. 71 — Redesign the fish interactive simulation using the following data
definition for the world:
A world is an image, either:
1.
2.
3.
4.
This development suggests the design recipe for video game and interactive anima-
tions displayed in Fig. 56. Steps 1–3 are problem and data analysis that are performed
once. Step 1 requires a data definition for the elements that vary in a game. Step 2
140 6 Aliens Attack Version 1
requires the development of a function template and of examples for each data def-
inition. If the defined data has variety, then at least one example for each variety is
needed. Step 3 requires the development of the run function. The body must be a
bb-expr. This function serves as a map to guide the top-down development of the
game. Ask yourself what events may affect the evolution of the game. There must be
a clause in the bb-expr for every event that may change the evolution of the game or
interactive simulation. This function is written without tests because it is only used
to initiate the game or simulation. It is not the solution to a problem. In fact, the run
function’s template may be defined as follows:
; string → world
; Purpose: To run the game
(define (run a-name)
(big-bang INIT-WORLD
[on-draw . . .]
[name a-name]
bb-clause∗))
The final clauses, of course, may not contain a repetition of any clause.
Steps 4–10 are performed for each function that is needed. This is done by
specializing the function template for the data being processed. These steps are the
same as those found in previous design recipes.
The first refinement to Aliens Attack is to add the rocket. Figure 57 displays a scene
of the proposed new version of the video game. Before starting save your Aliens
Attack version 0 to a new file, say Aliens-Attack-v1. It is this new file that is to
be edited with refinements. The version 0 is kept as a safe back-up of work that has
been completed.
36 Adding the Rocket to Aliens Attack 141
Recall that the rocket may move left to right without going off the edges of the
scene. Given that a rocket may exhibit changes as the game evolves, it needs to be
part of the world and, therefore, a data definition for a rocket is needed. Ask yourself
what changes about the rocket as the game advances. The characteristics that change
must be captured in the rocket’s data definition. At this point, it is important to
realize that a rocket is not the same as the image of the rocket. The image of the
rocket does not change as the rocket moves. When the rocket moves left or right the
rocket’s image-x changes. For example, if the image coordinates of the rocket are (5,
ROCKET-Y) and the rocket moves right, then the new image coordinates of the rocket
are (6, ROCKET-Y). That is, the rocket moves to the next image box to the right in
the scene. Observe, as noted in Chap. 6, that the rocket’s image-y is constant. Now
that the rocket’s changing characteristics are identified, the rocket’s data definition,
the template for functions on a rocket, and sample instances (rocket examples) may
be stated as follows:
#| A rocket is an image-x
;; rocket . . . → . . .
;; Purpose: . . .
(define (f-on-rocket a-rocket . . .)
. . .(f-on-image-x a-rocket . . .) . . .)
;; world . . . --> . . .
;; Purpose: . . .
(define (f-on-world a-world . . .)
. . .(f-on-rocket a-world . . .) . . .)
36 Adding the Rocket to Aliens Attack 143
;; key . . . --> . . .
;; Purpose: . . .
(define (f-on-key a-key . . .)
(cond [(key=? a-key "right") . . .]
[(key=? a-key "left") . . .]
[else . . .]))
* Ex. 74 — Develop function templates for ci, image-x, image-y, pixel-x, pixel-
y, and scene.
After completing steps 1 and 2 of the design recipe, we focus on the run template
specialization to complete step 3. The player moves the rocket using the left and
right arrows keys. This means that an on-key clause is also needed to process key
events. The specialization is as follows:
36 Adding the Rocket to Aliens Attack 145
; string → world
; Purpose: To run the game
(define (run a-name)
(big-bang INIT-WORLD
[on-draw draw-world]
[name a-name]
[on-key process-key]))
This states that the handler to render the game’s scene is draw-world and that the
handler to process key events is process-key.
Let us start by designing draw-world. To draw the world a rocket needs to be
drawn in the empty scene. The steps of the design recipe result in the following
specialization of the template for functions on a world:
;; world → scene
;; Purpose: To draw the world in E-SCENE
(define (draw-world a-world)
(draw-rocket a-world E-SCENE))
(check-expect (draw-world 0) )
(check-expect (draw-world (sub1 MAX-CHARS-HORIZONTAL))
)
146 6 Aliens Attack Version 1
)
(check-expect (draw-rocket (sub1 MAX-CHARS-HORIZONTAL)
E-SCENE)
)
36 Adding the Rocket to Aliens Attack 147
The sample expressions use draw-ci to place the rocket’s ci at image coordinates
(3, ROCKET-Y) and (12, ROCKET-Y) in, respectively, E-SCENE and E-SCENE2.
The two differences among the sample expressions are abstracted and become the
parameters to draw-rocket. The tests using sample values, once again, test rocket
extrema values. There are no new auxiliary functions to design. Therefore, this
concludes the design of draw-world and its auxiliary functions.
The design of process-key is done by specializing the f-on-key template. The
universe API requires that the inputs be a world and a key. If the given key is
"right" or "left", then the next world is created by moving the rocket. Otherwise,
the next world is the given world. The specialization of the template yields:
;; world key → world
;; Purpose: Process a key event to return next world
(define (process-key a-world a-key)
(cond [(key=? a-key "right") (move-rckt-right a-world)]
[(key=? a-key "left") (move-rckt-left a-world)]
[else a-world]))
** Ex. 75 — Redesign Aliens Attack version I using the following data defini-
tion for a rocket:
A rocket is an image-x such that it is either:
1. 0
2. (sub1 MAX-CHARS-HORIZONTAL)
3. (0..(sub1 MAX-CHARS-HORIZONTAL))
*** Ex. 77 — Design an interactive traffic light simulation such that if the
player presses:
1."r" the traffic light goes to flashing red.
2."y" the traffic light goes to flashing yellow.
3."n" the traffic light goes to normal operation.
• The universe’s big-bang expression is used to run a video game and allows
a programmer to provide the world’s initial value and the handlers needed to
advance the game’s or animation’s evolution.
• Predicates for data types with varieties sometimes may be simplified by using a
Boolean expression instead of a conditional expression.
• The video game design recipe includes steps to:
– Develop data definitions and sample instances for every element that varies as
the game evolves.
– Develop a function template for every data definition.
– Develop a run function.
• Separation of concerns leads to functions that solve a problem for a single data
type and simplifies the process of iterative refinement.
• Developing video games is intellectually stimulating and exciting.
Part II
Compound Data of Finite Size
Chapter 7
Structures
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 153
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_7
154 7 Structures
(2,3)
(-1,1)
x
(0,0)
(-4,-3)
(4,-5)
y
integers and not a single point. You may say that it is understood that the two integers
represent a point. Perhaps, that is clear to you now. Will it be clear to other people
who may read your code? How about to yourself 6 months from now? The truth is that
for a two-dimensional point these incongruities may be relatively easy to overcome.
What if you were solving a problem using Einstein’s Mathematics in which every
point has 4 dimensions or in string theory where a point has 10 dimensions? Should
every string theory point-processing function have 10 inputs? That certainly begins
to sound problematic.
The problem with the above solution is that it does not exhibit separation of
concerns. We need a function that takes as input a two-dimensional integer point.
We also need a function to process an integer x coordinate and another function to
process an integer y coordinate. Our immediate need, therefore, is a mechanism to
create a point value out of two coordinates. With such a mechanism, we may rewrite
in-Q1? as something similar to
;; 2D-ipoint → Boolean
;; Purpose: To determine if the given 2D-ipoint is in Q1
(define (in-Q1? a-2dipoint)
(and (x-in-Q1? (x-of a-2dipoint))
(y-in-Q1? (y-of a-2dipoint))))
Most will argue that the above function, if it were possible to write, is easier to
understand than the previous version. It is likely to be as clear 6 months from now
as it is today. To write such nifty functions, it is necessary to learn more about BSL.
In BSL, compound data of finite size may be represented as a single piece of data
using a structure. A structure combines a fixed number of values into a single piece
of data. In addition, for every structure there is a constructor and there is a selector
for each characteristic. A constructor is a function to build instances of the compound
data. A selector retrieves the value of a characteristic.
For instance, BSL provides the posn structure. A posn is intended to represent a
position in a Cartesian plane. In other words, it is intended to represent what is called
a 2D-point (not to be confused with a 2D-ipoint). A posn has two characteristics
(or fields) called x and y. The constructor for a posn is make-posn. It requires two
inputs: one for x and one for y. There are two selector functions: posn-x to retrieve
the value of x and posn-y to retrieve the value of y. Run the following example in
DrRacket:
(define A-POSN (make-posn 3.2 9))
(define A-POSN2 (make-posn -8 9))
Fig. 59 The data definitions and function templates for the 2D-point program
#|;; An x-coordinate (xcoord) is a real number
;; A y-coordinate (ycoord) is a real number
;; A 2D-point is a structure (make-posn xcoord ycoord)
(define XCOORD1 . . .) . . .
(define YCOORD1 . . .) . . .
(define 2D-POINT1 . . .) . . .
to functions that process an xcoord and a ycoord. Based on this data analysis, the
2D-point program above may be refined to
;; Sample instances of xcoord
(define X1 3.2)
(define X2 -8)
(define Y1 27)
(define Y2 39)
;; 2D-point . . . → Boolean
;; Purpose: Determine if the given 2D-point is on the
;; graph of f(x) = x3
(define (on-x^3? a-2Dpoint)
. . .(f-on-xcoord (posn-x a-2Dpoint)). . .)
sample expressions is the 2D-point processed. Therefore, on-x3 only needs one
parameter.
Tests using sample values are added for testing thoroughness. The complete
template specialization is
(define X1 3)
(define X2 -2.5)
(define Y1 27)
(define Y2 39)
;; 2D-point → Boolean
;; Purpose: Determine if the given 2D-point is on the
;; graph of f(x) = x3 3
(define (on-x^3? a-2Dpoint)
(= (cube (posn-x a2D-point)) (posn-y a-2Dpoint)))
Before proceeding with another example, it is worth to take a pause to analyze the
result obtained for on-x3 . It is natural to ask if the solution may be simplified. This
39 Going Beyond the Design Recipe 161
step is not necessary but may be performed to aid code readability and/or to reduce
the amount of code.
The design steps for on-x3 revealed the need for and the subsequent design of
cube. The function cube, however, is rather simple and easy to understand. In fact,
defining cube may add little clarity to the program—this, of course, is a matter of
opinion. If you feel that function does not add clarity to a program, you may want
to consider inlining it. In this case, inlining is straightforward and results in the
following refined version of the program:
(define X1 3)
(define X2 -2.5)
(define Y1 27)
(define Y2 39)
;; 2D-point → Boolean
;; Purpose: Determine if the given point is on the graph
;; of f(x) = x^3
(define (on-x^3? a-2Dpoint)
(= (expt (posn-x a-2Dpoint) 3) (posn-y a-2Dpoint)))
it was used 100 times. Inlining in this scenario means that similar code is repeated
100 times in the program. Remember that in Chap. 3, this is precisely the situation we
wished to avoid. It is better to abstract over similar expressions to create a function
than to repeatedly have similar expressions throughout the code. Beyond eliminating
repetitions, abstraction over similar expressions also makes revisions easier. Imagine
that a better way to compute x3 is discovered. If cube is inlined 100 times, then the
revision to use the better way to compute x3 requires 100 edits. On the other hand, if
cube is not inlined, then only one edit is required. A good problem solver foresees
such possibilities and does not forsake separation of concerns in favor of having to
design fewer functions.
When should inlining be considered? If a function is only used once and its design
does not require any type of specialized knowledge, then you may consider inlining.
Keep in mind, however, that inlining is usually left to a compiler. As problem solvers,
the rule of thumb is to avoid inlining.
40 Revisiting in-Q1?
Fig. 61 The first three steps of the design recipe defining 2D-ipoint
#| ;; An x-coordinate (ix) and a y-coordinate (iy) are integers
;; A 2D-ipoint is a structure (make-posn ix iy).
;; Sample instances of ix: (define IX1 . . .) . . .
;; Sample instances of iy: (define IY1 . . .)
;; Sample instances of 2D-ipoint: (define 2D-IPOINT1 (make-posn . . . . . .)). . .
;; ix . . . ... Purpose: . . .
(define (f-on-ix an-ix . . .) . . . an-ix. . .)
;; Sample expressions for f-on-ix
(define IX-VAL . . .)
;; Tests using sample computations for f-on-ix
(check-expect (f-on-ix . . .) IX-VAL) ...
;; Tests using sample values for f-on-ix
(check-expect (f-on-ix . . .) . . .) ...
;; iy . . . ... Purpose: . . .
(define (f-on-iy a-iy . . .) . . . a-iy. . .)
;; Sample expressions for f-on-iy
(define IY-VAL . . .) ...
;; Tests using sample computations for f-on-iy
(check-expect (f-on-iy . . .) IY-VAL) ...
;; Tests using sample values for f-on-iy
(check-expect (f-on-iy . . .) . . .) ...
;; 2D-ipoint . . . ... Purpose: . . .
(define (f-on-2D-ipoint a-2Dipoint . . .)
. . .(f-on-ix (posn-x a-2Dipoint)). . .(f-on-iy (posn-y a-2Dipoint)). . .)
;; Sample expressions for f-on-2D-ipoint
(define 2D-IPOINT-VAL . . .) ...
;; Tests using sample computations for f-on-2D-ipoint
(check-expect (f-on-2D-ipoint . . .) 2D-IPOINT-VAL) ...
;; Tests using sample values for f-on-2D-ipoint
(check-expect (f-on-2D-ipoint . . .) . . .) . . . |#
;; Sample instances of ix
(define IX1 3) (define IX2 -5)
;; Sample instances of iy
(define IY1 11) (define IY2 23)
;; Sample instances of 2D-ipoint
(define 2D-IPOINT1 (make-posn IX1 IY1))
(define 2D-IPOINT2 (make-posn IX2 IY2))
;; iy → Boolean
;; Purpose: To determine if the given iy is in the Q1 range
(define (iy-in-Q1? an-iy)
(> an-iy 0))
It turns out that rarely is the input to a function, for example, a single string or
number. It is far more common for there to be several pieces of related data that
need to be processed. A 2D-point, for instance, may be represented using a posn
structure. A posn structure works well, because a 2D-point only has two varying
characteristics. What needs to be done if data has more than two (but finite) varying
characteristics? Clearly, a structure with two fields, like a posn, cannot be used to
represent such data.
Consider representing a student that has a first name, a middle name, a last name,
and a grade point average. How can a student be represented? What is needed is a
structure that has four fields: one for each varying characteristic. You may imagine
that BSL provides a structure with four fields, but it does not. There is actually a
good reason for this. As you can imagine, the variety of compound data of fixed
size is infinite. Everybody can imagine data with 2, 3, 4, 5, 6, or more varying
characteristics. There is no way BSL can provide structures of all possible sizes.
Therefore, BSL gives programmers the ability to define their own structures. In this
manner, a programmer can define finite compound data of any size and provide
custom names to the structure and its fields. The first step is to learn the necessary
BSL syntax to define structures.
42 Defining Structures
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 167
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_8
168 8 Defining Structures
A structure definition creates much more than just a structure that can store
multiple values. It also creates the constructor and selector functions for the structure.
The naming conventions for these functions is straightforward. The constructor is
always named:
make-<structure name>
The number of arguments required by a constructor is equal to the number of fields
the structure has. The selectors are always named:
<structure name>-<field name>
The input to a selector must be an instance of <structure name>.
We can now define a structure for a 2D-point as follows:
(define-struct 2Dpoint (xval yval))
This structure definition creates the following functions to manipulate 2D-points:
;; X Y → 2Dpoint
;; Purpose: To create a 2Dpoint with the given values
(define (make-2Dpoint an-X an-Y) . . .)
;; 2Dpoint → X
;; Purpose: Return the xval of the given 2Dpoint
(define (2Dpoint-xval a-2Dpoint) . . .)
;; 2Dpoint → Y
;; Purpose: Return the yval of the given 2Dpoint
(define (2Dpoint-yval a-2Dpoint) . . .)
;; Any → Boolean
;; Purpose: To determine if the given value is a 2Dpoint
(define (2Dpoint? any-value) ...)
Do not worry about how these functions are implemented in BSL. There is no need to
know these details. It is important, however, to understand the structure API provided
by BSL. The first function is called a constructor. A constructor is a function that
is used to create data type instances. The rest of the functions are observers. An
observer is a function that tells us something about an instance of a data type. The
last function, 2Dpoint?, is a predicate observer that distinguishes between data that
is a 2Dpoint and data that is not a 2Dpoint.
Take note that the contracts for the constructor and the selectors refer to inde-
terminate types called X and Y. X and Y are type variables and represent a defined
type that exists in BSL or that has been defined by a programmer. The type variables
indicate that a structure is generic. This means that it works equally well for many
different data types. BSL assumes nothing about the type of data that the structure
stores. Genericity provides programmers with a great deal of flexibility, but must be
used with care. For example, a programmer may think that the following are valid
2D-points:
42 Defining Structures 169
;; 2Dpoint . . . → . . .
;; Purpose: . . .
(define (f-on-2Dpoint a-2dpoint . . .)
. . .(2Dpoint-xval a-2dpoint). . .(2Dpoint-yval a-2dpoint). . .)
formula. Given that one of the points, the origin, is always (0, 0) the distance formula
may be simplified as follows:
distance((x1, y1 ), (x2 , y2 )) = (x2 − x1 )2 +(y2 − y1 )2
distance((0, 0), (x2 , y2 )) = (x2 − 0)2 +(y2 − 0)2
= x22 + y22
;; 2Dpoint → R
;; Purpose: To compute the distance to the origin for the
;; given 2Dpoint
(define (distance-to-origin a-2dpoint)
. . .(2Dpoint-xval a-2dpoint). . .(2Dpoint-yval a-2dpoint). . .)
;; 2Dpoint → real-number
;; Purpose: To compute the distance to the origin for the
;; given 2Dpoint
(define (distance-to-origin a-2dpoint)
(sqrt (+ (sqr (2Dpoint-xval a-2dpoint))
(sqr (2Dpoint-yval a-2dpoint)))))
43 Computing Structures
Just like numbers, Booleans, strings, and posns, the structures a programmer defines
are first class is BSL. First class means that they may be passed as input to functions
and may be returned as a function value. In other words, functions may compute
instances of a structure.
To illustrate the computation of structures consider the problem of updating a
student’s grade point average. A student, as suggested by the description at the
172 8 Defining Structures
beginning of this chapter, has a first name, a middle name, a last name, and a grade
point average. This leads to the following data definitions, structure definition, and
function templates:
#| DATA DEFINITIONS
A GPA is a real number in [0..4].
A student is a structure
(make-student string string string R)
that contains a first name, a middle name, a last name,
and a grade point average.
#| FUNCTION TEMPLATES
;; GPA . . . → . . .
;; Purpose: . . .
(define (f-on-GPA a-gpa . . .) . . .)
;; student . . . → . . .
;; Purpose: . . .
(define (f-on-student a-student . . .)
. . .(f-on-string (student-fn a-student)). . .
. . .(f-on-string (student-mn a-student)). . .
. . .(f-on-string (student-ln a-student)). . .
43 Computing Structures 173
. . .(f-on-GPA(student-gpa a-student)). . .)
Observe that the sample expressions create new instances of students that retain
the names of an existing student structure and that use one of the new GPA sam-
ple instances. There are two differences among the sample expressions: a student
and a GPA. This informs us that update-student-gpa only needs two parameters.
Further observe, that neither a string nor a GPA is processed. Therefore, there is no
need to call string or GPA processing functions. The tests using sample computa-
tions utilize the sample students, the sample (new) GPAs, and the values of the
evaluated sample expressions. This leads to the following final specializations for
update-student-gpa and the tests:
;; student real → student
;; Purpose: Update the given student’s gpa
(define (update-student-gpa a-student a-gpa)
(make-student (student-fn a-student)
(student-mn a-student)
(student-ln a-student)
a-gpa))
and
The string for the student’s middle name abbreviation and the string for the student’s
grade point average need to be computed. Once computed, all the strings may be
appended to create the returned string. This analysis allows us to start specializing
the template for functions on a student. We start with developing sample expressions:
;; Sample expressions for student2string
(define STUD1-STR (string-append
(student-fn STUD1)
(middle-name-abbrev (student-mn STUD1))
(student-ln STUD1)
" has a "
(gpa->string (student-gpa STUD1))
" grade point average."))
(check-expect
(student2string (make-student "Manuel" "" "Núñez" 3.89))
"Manuel Núñez has a 3.89 grade point average.")
Let us continue with the task of designing the auxiliary function middle-name
-abbrev. We can observe that not every student has a last name. In this case, the
middle name string ought to be " " to provide a space in between the first and the last
names. If a student has a middle name, then the string ought to be " " followed by the
first letter of the middle name followed by ". ". Note that this scheme provides for
spaces before and after the middle name abbreviation. Our analysis clearly suggests
that there is variety in the data called middle name and, therefore, a data definition
is required:
A middle name (mn) is either:
1. ""
2. not ""
The corresponding function template is:
;; Sample instances for mn
(define MN1 "")
(define MN2 . . .)
...
;; student . . . → . . .
;; Purpose: . . .
(define (f-on-student a-student . . .)
. . .(f-on-string (student-fn a-student)). . .
. . .(f-on-mn (student-mn a-student)). . .
. . .(f-on-string (student-ln a-student)). . .
. . .(f-on-GPA (student-gpa a-student)). . .)
These refinements make it clear to anyone reading your code what a student is and
how a student might to be processed. Observe that the middle name is now an nm
and that the middle name may be processed by calling a function that processes an
nm.
We can now proceed with the design of middle-name-abbrev. If the middle
name is "", then the middle name abbreviation is clearly " " according to our design.
Otherwise, the middle name abbreviation is a space followed by the first letter of the
given mn followed by a period and ending with a space. Sample instances of mn and
sample expressions for f-on-mn are:
;; Sample instances of mn
(define MN1 " ")
(define MN2 "Jose")
(define MN3 "Francisco")
Actual value
"Mercedes G. Merayo has a 397/100 grade point average."
differs from
"Mercedes G. Merayo has a 3.97 grade point average.",
the expected value.
44 Structures for the Masses 181
Actual value
"Manuel Núñez has a 389/100 grade point average."
differs from
"Manuel Núñez has a 3.89 grade point average.",
the expected value.
The first four failures are from tests involving gpa->string and the last two failures
are from tests using sample values for student2string. What happened? Where
did those fractions in the actual values come from? Where do we start refining our
solution? To answer the first two questions we must recall the discussion of numbers
in Chap. 2 and better understand how BSL stores numbers. Whenever possible, BSL
stores a numerical value as exact (instead of inexact) number. This is done in an
effort to avoid errors involving computations with inexact numbers. Therefore, a real
number that may be exactly represented is stored as an integer or a fraction. For
example, 4.0 is stored as the integer 4 and 3.8 is stored as the fraction 19 5 . This
explains the test failures for gpa->string.
Observe that the same error is detected by the tests using sample values for
student2string. Why is the error not detected by tests using sample computations
for student2string? The answer lies in realizing that these tests check if a function
computes the same values as the sample expressions. Thus, these tests are unable to
detect errors in the sample expressions. Simply stated, the sample expressions using
concrete values evaluate to the same erroneous result as using student2string.
When abstracting over sample expressions, a logical error in the development of
the expressions is test-wise consistent with the use of a function created by the
abstraction. On the other hand, the tests using sample values break away from testing
that functions and expressions evaluate to the same value. Instead, they test that
functions evaluate to the right value. This explains why only the tests using sample
values detect the error. This exercise highlights the importance of always writing
tests using sample values.
To answer the third question, a good rule of thumb is to always start debugging
refinements with the auxiliary functions. An auxiliary function bug that manifests
itself in the tests for the function usually also manifests itself in the tests for functions
that use said auxiliary function. Therefore, fixing the auxiliary function may fix the
bugs in the functions that call the auxiliary function.
Using this principle, we start the debugging process with gpa->string. The
problem is that this function needs to return a string representing an inexact number,
not an exact number. Therefore, a function to transform an exact number to an inexact
number is needed. Exploring the BSL page in the Help Desk reveals the following
function:
number → number
Purpose: Converts an exact number to an inexact one.
(define (exact->inexact x) ...)
182 8 Defining Structures
This function looks like what is needed. The refined gpa->string is:
;; GPA → string
;; Purpose: Transform the given GPA to a string
(define (gpa->string a-gpa)
(number->string (exact->inexact a-gpa)))
Running the program reveals that all the tests pass. This completes the design of
student2string.
*** Ex. 87 — All cars have five characteristics: a brand, a model, a color, a
manufacturing year, and a maximum speed in miles per hour. Design and write
a program to convert a car to a descriptive string. For example, a car with the
following characteristics:
Brand: Alpha Romeo
Model: Giulia
Color: Rosso competizione
Year: 2020
Speed: 160
is converted to:
The rosso competizione 2020 Alpha Romeo Giulia has a max speed
of 160 mph.
17
This exercise is due to a discussion with Robby Findler.
45 What Have We Learned in This Chapter? 183
The ability to define and solve problems using structures endows us with the power
to refine Aliens Attack version 1 (from Chap. 6). This refinement adds an alien to
the game. The following is a sample scene for Aliens Attack version 2:
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 185
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_9
186 9 Aliens Attack Version 2
need to be written. Some functions do not need to be refined and are used unchanged
in this new version of the game.
The plan is to follow the steps of the design recipe for video games in Fig. 56.
For every handler, a top-down design approach is followed. As the design advances,
functions that process instances of a refined data definition are updated. New auxiliary
functions are developed as their need arises.
46 Data Definitions
Sometimes it is easier to start defining the simplest data first. In this case, this means
starting with the data definition of an alien. To define an alien think about what may
change from one instance on an alien to another. An alien moves as follows:
Think carefully about what changes when the alien is moved right, left, or down.
Clearly, the image of the alien does not change. As we know, the image of the alien
is a constant. Therefore, an alien is not and does not contain an image. What changes
when the alien moves right? If you think about it for a while, it becomes clear that
the x coordinate of the alien changes. Given that this is an x coordinate in an Aliens
Attack scene, it has to be an image-x (defined in Fig. 4). What changes when the
alien moves left? Clearly, it is an image-x again. What changes when the alien
moves down? The alien’s image-x does not change. Instead, its image-y changes.
There is nothing more that changes about the alien. Therefore, we now know that an
alien has two characteristics that change: its image-x and its image-y. Now, think
about how an alien may be represented. Using a posn to represent an alien seems to
be a natural fit. Therefore, we may define an alien as follows:
#| An alien is a posn: (make-posn image-x image-y). |#
Next think about what changes in the world as the game evolves. Just as before,
the rocket may change. It is also fairly straightforward to see that the alien changes.
Does anything else change? Think about this carefully for a moment. Although not
rendered in a game scene, the direction the alien travels in changes. Sometimes the
alien is moving right. Sometimes it is moving left. Sometimes it is moving down.
Given that nothing else changes in the world, we have that the world has three
characteristics that may vary as the game evolves: the rocket, the alien, and the
direction that the alien travels in. It now becomes clear that a data definition for a
direction and a new data definition for a world are needed.
47 Function Templates and Sample Instances 187
Again, start by defining the simplest data. Given that a direction can only be one
of three values, we may define it as an enumerated type:
#| A direction (dir) is either:
1. 'right
2. 'left
3. 'down |#
This data definition is clearly stating that a dir may only be one of three symbols
at any point during the game’s evolution. Making a direction anything else is an
improper use of a direction.
We now turn our attention to defining a world. As noted above, a world has
three characteristics that may vary. Three is a finite number greater than one. This
immediately suggests using a structure with three fields. This means that the world
is no longer a rocket. Instead, the world is defined as follows:
#| A world is a structure: (make-world rocket alien dir). |#
This data definition clearly states that a world is composed of three fields such
that the first fields is a rocket, the second field is an alien, and the third field is a
dir. If any of these fields is made something different, then it is an improper use of
a world structure. Given that we have a data definition for a new type of structure,
the program must contain a structure definition for it. The world structure is defined
as follows:
(define-struct world (rocket alien dir))
Observe that the name of the structure and the name of each field is suggestive of
the data type they represent.
There are two new data definitions and one refined data definition. This means we
need two new function templates and need to refine one function template. The
template for functions on an alien and instances of alien are:
#| ;; alien . . . → . . .
;; Purpose: . . .
(define (f-on-alien an-alien . . .)
. . .(f-on-image-x (posn-x an-alien). . .)
. . .(f-on-image-y (posn-y an-alien). . .))
;; Sample instances
(define ALIEN1 (make-posn . . . . . .))
We start code refinement with the run function because it informs us of the world-
processing handlers needed. This means we are following a top-down approach to
problem solving. It is necessary to determine what functions are needed in this new
version of Aliens Attack with its new world data definition. In Chap. 6, the run is
defined as follows:
; string → world
; Purpose: To run the game
(define (run a-name)
(big-bang INIT-WORLD
[on-draw draw-world]
[name a-name]
[on-key process-key]))
Ask yourself if draw-world and process-key are still needed. Ask yourself is
any other handlers are needed. Given that the world must still be rendered to the
screen and that the player must still be able to move the rocket, it is clear that both
draw-world and process-key are still needed. These functions, however, need to
be refined because they process a world whose data definition has been refined.
To determine if other handlers are needed it is necessary to think about how the
game ought to work. The world’s new element is the alien. what is the expected
behavior of the alien in the game? After some thought, it becomes clear that the
player does not control the movements of the alien. Instead, the alien must move
every time the clock ticks. This means that a clock tick handler is needed. Further
thought reveals that it is possible for this version of the game to end. It ends when the
alien reaches earth. This means that a handler to detect the end of the game is also
needed. We must associate these two new handlers with the game in the big-bang
expression. This leads to this refined version of the run function:
49 Drawing the World 191
; string → world
; Purpose: To run the game
(define (run a-name)
(big-bang INIT-WORLD
[on-draw draw-world]
[name a-name]
[on-key process-key]
[on-tick process-tick TICK-RATE]
[stop-when game-over?]))
The handler to process clock ticks is process-tick and the handler to detect the
end of the game is game-over?. Observe that an optional argument is given to
on-tick. By limiting the number of clock ticks per second the speed at which the
alien moves is controlled. In this example, the clock ticks 4 times per second or,
equivalently, every 14 seconds. You may, of course, adjust the value of TICK-RATE
to your liking to make the alien move faster or slower.
The problems that we must solve are now clear. We must refine draw-world and
process-key and we must design process-tick and game-over?. In addition, it
may also be necessary to write new auxiliary functions.
To start, we tackle the problem of drawing the world. It is almost certain that you have
already realized that a function to draw an alien is needed. This is good insight.
You should not, however, immediately jump to write such a function. Instead, be
disciplined about designing your solution to drawing the world. Follow a top-down
design strategy and start with the handler to draw the world. Allow the design process
to reveal all the auxiliary functions that are needed and design them when the need
arises.
(check-expect
(draw-world (make-world INIT-ROCKET2 INIT-ALIEN INIT-DIR3))
)
(check-expect
(draw-world (make-world INIT-ROCKET INIT-ALIEN2 INIT-DIR2))
The design steps reveal the need for draw-alien. This function processes an alien.
Thus, we can specialize the template for functions on an alien to implement it. Think
about how to draw the alien. You have two inputs: an alien and a scene. To draw the
image of the alien character we may use draw-ci (just as is done to draw the rocket’s
character image). The image coordinates are obtained from the alien to draw. This
suggests the following initial template specialization:
;; alien scene → scene
;; Purpose: Draw the given alien in the given scene
(define (draw-alien an-alien scn)
. . .(f-on-image-x (posn-x an-alien). . .)
. . .(f-on-image-y (posn-y an-alien). . .))
)
There are no new auxiliary functions needed. Therefore, the required updates for
draw-world are done.
The player does not control or affect the aliens nor the direction the aliens travel in.
Therefore, as before, key events only affect the rocket. This means that process-key
does not have to process an alien nor a dir. The refinement of process-key only
requires refining the worlds created in the sample expressions, in the function
definitions, and in the tests. Start with the sample expressions. These must now
evaluate to a world structure. Use the defined sample world instances to refine
them as follows:
;; Sample expressions for process-key
(define KEY-RVAL (make-world
(move-rckt-right (world-rocket INIT-WORLD))
(world-alien INIT-WORLD)
(world-dir INIT-WORLD)))
(define KEY-LVAL (make-world
(move-rckt-left (world-rocket INIT-WORLD))
(world-alien INIT-WORLD)
(world-dir INIT-WORLD)))
(define KEY-OVAL INIT-WORLD2)
Observe that in the sample expressions the alien and the dir are left unchanged in
the new world created. As expected, only the rocket may be changed. The rocket,
however, is not always changed and this is illustrated by the third test. The rocket
is not moved when, for example, m is pressed by the player. Therefore, a new world
is not created and an unchanged existing world is used to define the third constant.
Based on these new sample expressions, it is straightforward to refine the function
definition. The process-key body now creates worlds as follows:
;; world key → world
;; Purpose: Process a key event to return next world
(define (process-key a-world a-key)
(cond [(key=? a-key "right")
196 9 Aliens Attack Version 2
(make-world
(move-rckt-right (world-rocket a-world))
(world-alien a-world)
(world-dir a-world))]
[(key=? a-key "left")
(make-world
(move-rckt-left (world-rocket a-world))
(world-alien a-world)
(world-dir a-world))]
[else a-world]))
The tests using sample computations, as before, remain unchanged given that they
only reference constants:
;; Tests using sample computations for process-key
(check-expect (process-key INIT-WORLD "right") KEY-RVAL)
(check-expect (process-key INIT-WORLD "left") KEY-LVAL)
(check-expect (process-key INIT-WORLD2 "m") KEY-OVAL)
The tests using sample values are updated to use world structures. These tests
illustrate that the rocket may not move off the scene (the first two tests) and that keys
other than "left" and "right" beyond "m" leave the input world unchanged (the
last two tests). The updated tests are:
;; Tests using sample values for process-key
(check-expect (process-key
(make-world (sub1 MAX-CHARS-HORIZONTAL)
INIT-ALIEN
'right)
"right")
(make-world (sub1 MAX-CHARS-HORIZONTAL)
INIT-ALIEN
'right))
(check-expect (process-key (make-world 0
INIT-ALIEN
'left)
"left")
(make-world 0
INIT-ALIEN
'left))
(check-expect (process-key (make-world 0
INIT-ALIEN
'left)
"o")
(make-world 0
INIT-ALIEN
'left))
(check-expect (process-key INIT-WORLD2 ";") INIT-WORLD2)
51 Processing Ticks 197
The refinement process does not reveal the need for new auxiliary functions.
Therefore, the refinements for process-key are complete.
51 Processing Ticks
To design the process-tick handler think carefully about how the game must
evolve every time the clock ticks. What changes? What does not change? The rocket
is not affected by clock ticks given that the rocket only moves on keystrokes made
by the player. The alien, on the other hand, must move every clock tick. This means
that we need to design a function to move an alien.
A bit more subtle is the fact that when the alien moves the direction the new alien
must move in may be different. This means that a function to compute the direction
for the new alien is needed. This function must distinguish when not to change the
direction and between the cases to change the direction. Careful analysis reveals that
the direction only changes when the alien is at either the left or right edge of the
scene. We can specify how the direction changes as follows:
• New alien at right edge created by moving right means the new direction is down
• New alien at left edge created by moving left means the new direction is down
• New alien at right edge created by moving down means the new direction is left
• New alien at left edge created by moving down means the new direction is right
• Otherwise, the direction does not change.
This analysis suggests that to compute the direction of a new alien two pieces of data
are required: the new alien and the direction used to create the new alien.
Based on the above problem analysis, the specialization of the template for a function
on a world to design process-click is started. The first step is to specialize based
on the analysis so far:
;; world → world
;; Purpose: Create a new world after a clock tick
(define (process-tick a-world)
. . .(f-on-rocket (world-rocket a-world) . . .)
. . .(f-on-alien (world-alien a-world) . . .)
. . .(f-on-dir (world-dir a-world) . . .))
(new-dir-after-tick
(move-alien (world-alien INIT-WORLD)
(world-dir INIT-WORLD))
(world-dir INIT-WORLD))))
(define AFTER-TICK-WORLD2
(make-world (world-rocket INIT-WORLD2)
(move-alien (world-alien INIT-WORLD2)
(world-dir INIT-WORLD2))
(new-dir-after-tick
(move-alien (world-alien INIT-WORLD2)
(world-dir INIT-WORLD2))
(world-dir INIT-WORLD2))))
(check-expect (process-tick
(make-world
INIT-ROCKET2
(make-posn (- MAX-CHARS-HORIZONTAL 2) 10)
'right))
(make-world INIT-ROCKET2
(make-posn MAX-IMG-X 10)
'down))
(make-world INIT-ROCKET2
(make-posn MIN-IMG-X 3)
'right))
The sample expressions are written using (to be designed) functions to move an
alien and to compute a new direction. The arguments to move an alien are fairly
straightforward to determine: the alien to move and the direction to move. If more
arguments are needed the design of move-alien will reveal them. The arguments
for new-dir-after-tick are based on the problem analysis above. The design of
new-dir-after-tick will reveal if more arguments are needed. The tests using
sample computations are written, as expected, using previously defined constants.
In this case, these tests illustrate when the alien does not change direction. The tests
using sample values are written building world instances. These tests illustrate that
the changes in direction occur as expected when an alien is at either edge of the
scene.
The next task is to specialize the body for process-tick. As done before, this
is achieved by abstracting over the sample expressions. This yields the following
function:
;; world → world
;; Purpose: Create a new world after a clock tick
(define (process-tick a-world)
(make-world
(world-rocket a-world)
(move-alien (world-alien a-world) (world-dir a-world))
(new-dir-after-tick (move-alien (world-alien a-world)
(world-dir a-world))
(world-dir a-world))))
Given that testing aliens on the left and right edges is needed, the following
instances of aliens are defined to facilitate the writing of sample expressions and
tests:
(define LEFT-EDGE-ALIEN (make-posn MIN-IMG-X 10))
(define RIGHT-EDGE-ALIEN (make-posn MAX-IMG-X 6))
Observe that there are two separate concerns. The first is to determine the given
direction instance. Second, is to compute the new direction. The computation of the
new direction ought to be split among different auxiliary functions depending on the
given direction:
1. new-dir-after-down is used when the given direction is down.
2. new-dir-after-left is used when the given direction is left.
3. new-dir-after-right is used when the given direction is right.
Based on this design idea we can specialize the template for a function on a direction.
Sample expressions may be written as follows:
;; Sample expressions for new-dir-after-tick
(define NEW-DIR-LEDGE-ALIEN-DOWN
(new-dir-after-down LEFT-EDGE-ALIEN))
(define NEW-DIR-REDGE-ALIEN-DOWN
(new-dir-after-down RIGHT-EDGE-ALIEN))
(define NEW-DIR-INIT-ALIEN-LEFT
(new-dir-after-left INIT-ALIEN))
(define NEW-DIR-LEDGE-ALIEN-LEFT
(new-dir-after-left LEFT-EDGE-ALIEN))
(define NEW-DIR-INIT-ALIEN-RIGHT
(new-dir-after-right INIT-ALIEN))
(define NEW-DIR-REDGE-ALIEN-RIGHT
(new-dir-after-right RIGHT-EDGE-ALIEN))
Observe that there are two sample expressions for each auxiliary function. This is
due to the fact that for each possible direction the new direction may be one of two
values. For example, if the given direction is down, then the new direction may be
right if the given alien is at the left edge and may be left if the given alien is at the
right edge. Similarly, if the given direction is left, then the new direction is down
if the given alien is at the left edge and remains left otherwise. Finally, if the given
direction is right, then the new direction is down if the given alien is at the right
edge and remains right otherwise. In summary, there is a sample expression for each
possible outcome for a given direction.
Based on the conditions and the sample expressions from above the definition
template and tests are specialized as follows:
;; alien dir → dir
;; Purpose: Return new alien direction
(define (new-dir-after-tick an-alien old-dir)
(cond [(eq? old-dir 'right)
(new-dir-after-right an-alien)]
51 Processing Ticks 201
Carefully consider what the next direction ought to be if the previous direction is
down. When the previous direction is down we know that the given alien must be at
one of the edges. If the given alien is at the left edge, then the new direction is right.
If the given alien is at the right edge, then the new direction is left. Observe that
this function must determine which edge the alien is at. This is a different problem
from computing the new direction. In the spirit of separation of concerns, detecting
if an alien is at the left or the right edge is delegated to an auxiliary function. Further
observe that this means that new-dir-after-down does not process the given alien
and, therefore, the design of this function does not specialize the template for a
function on an alien. Instead, it is designed around the decision that must be made
based on the edge the alien is located at.
We are free to choose if the function detects the alien at the left edge or at the
right edge because if the detection for an edge fails, then the alien must be at the
other edge. We arbitrarily choose to detect if the alien is at the left edge. The function
using this strategy looks as follows:
;; alien → direction
;; Purpose: Compute the direction of the given alien
;; when previous direction is down
(define (new-dir-after-down an-alien)
(if (alien-at-left-edge? an-alien)
'right
'left))
expressions. The test using a sample value illustrates that the function works for an
extrema value: an alien at the right edge.
The design of the three previous functions reveals the need for two more auxiliary
functions. Let us start with the design of alien-at-left-edge?. How do we know
if an alien is at the left edge? A given alien is at the left edge if its image-x coordinate
51 Processing Ticks 205
is MIN-IMG-X. This means that the given alien must be processed and, therefore,
this function is designed by specializing the template for a function on an alien. This
specialization yields:
;; alien → Boolean
;; Purpose: Determine if he given alien is at the left edge
(define (alien-at-left-edge? an-alien)
(= (posn-x an-alien) MIN-IMG-X))
How do we know if an alien is at the right edge? A given alien is at the right edge
if its image-x coordinate is MAX-IMG-X. This means that the given alien must be
processed Once again, this is a function that is designed by specializing the template
for a function on an alien. This specialization yields:
;; alien → Boolean
;; Purpose: Determine if the given alien is at the
;; right edge
(define (alien-at-right-edge? an-alien)
(= (posn-x an-alien) MAX-IMG-X))
The next problem to solve is moving an alien. This is another interesting function
because we must decide how to design it. It takes two different types of data as input:
an alien and a dir. Should it be designed by specializing the template for functions
on an alien or the template for functions on a dir? To make this decision requires
further problem analysis.
Specializing the template for functions on a dir means that a new alien is created
by computing either a new image-x value or a new image-y value depending on
the given dir. A conditional is needed to determine which is computed. If the
51 Processing Ticks 207
direction is 'right or 'left, then the new alien is constructed with a new image-x
coordinate. If the direction is 'down, then the new alien is constructed with a new
image-y coordinate. In summary, for each possible direction either a new image-x
or a new image-y value must be computed.
Specializing the template for functions on a alien means that a new alien is
constructed by computing an image-x value using the given alien’s image-x coor-
dinate and the given direction and by computing an image-y value using the given
alien’s image-y coordinate and the given direction. A consequence of this design
path is that the functions to compute the new image-x and image-y coordinates
must have a conditional. The image-x value remains unchanged if the direction is
'down. The image-y value remains unchanged if the direction is either 'right or
'left.
As it turns out move-alien may be designed by specializing either template. In
such a case, you get to choose which design path to follow. Which design feels more
natural? Which design seems easier? Perhaps, specializing the template for functions
of a dir is easier. It only requires one conditional as opposed to two required by the
other design choice.
Let us choose to specialize the template for functions of a dir. This means
deciding how to compute a new alien for each variety of dir. We start by specializing
the template as follows:
;; alien dir → alien
;; Purpose: Move given alien in given direction
(define (move-alien an-alien a-dir)
(cond [(eq? a-dir 'right) . . .]
[(eq? a-dir 'left) . . .]
[else . . .]))
52 Subtyping
;; nni . . . → . . .
(define (f-on-nni an-nni) . . . an-nni . . .)
that instances of posint, POSINT1, and POSINT3 are used as nni arguments for x.
This can only be done because posint is a subtype of nni.
Subtypes may also be used for the image-x moving functions needed by
move-alien. For instance, the image-x values greater than MIN-IMG-X may be
defined as:
#|
An image-x>min is an image-x in [(add1 MIN-IMG-X)..MAX-IMG-X]
;; image-x>min . . . → . . .
;; Purpose: . . .
(define (f-on-image-x an-img-x>min . . .)
. . . an-img-x. . .)
;; image-x<max . . . → . . .
;; Purpose: . . .
(define (f-on-image-x<max an-img-x<max . . .)
. . . an-img-x. . .)
;; image-y<max . . . → . . .
;; Purpose: . . .
(define (f-on-image-y<max an-img-y<max . . .)
. . .˜an-img-y<max. . .)
;; image-y<max → image-y
;; Purpose: To move the given image-y<max down
(define (move-down-image-y an-img-y<max) (add1 an-img-y<max))
understand? This is a matter of ethics. We need to always strive to develop the easiest
code to understand and refine. Otherwise, ourselves and those that maintain our code
may have a harder job in the future.
(define MALIEN-VAL2-1
(make-posn (move-left-image-x (posn-x INIT-ALIEN))
(posn-y INIT-ALIEN)))
(define MALIEN-VAL2-2
(make-posn (move-left-image-x (posn-x INIT-ALIEN2))
(posn-y INIT-ALIEN2)))
(define MALIEN-VAL3-1
(make-posn (posn-x INIT-ALIEN)
(move-down-image-y (posn-y INIT-ALIEN))))
(define MALIEN-VAL3-2
(make-posn (posn-x (make-posn 1 8))
(move-down-image-y
(posn-y (make-posn 1 8)))))
(check-error
(move-alien (make-posn 7 MAX-IMG-Y) 'down)
(format "move-down-image-y: The character at y=~s cannot
move down."
MAX-IMG-Y))
The refinements needed by not defining the subtype posint are in italics to make
them easier to appreciate. They will not be in italics when typed into DrRacket.
The contract now clearly indicate that this function may throw an error. Observe that
there is a new sample expression to move an alien down that replaces the previous
test that used INIT-ALIEN2. The tests using sample computations are also refined
in the same manner. The most interesting refinements are to the tests using sample
values. Error tests have been added. The first error-test is written to make sure an
error is thrown and the correct error message is returned when an attempt is made to
move INIT-ALIEN2 down. Error-checks for inappropriately trying to move an alien
left or right are also added. An alien cannot be moved left if its image-x is 0 nor
can an alien be moved right if its alien-x is MAX-IMG-X.
Overall, it now becomes clear that defining subtypes simplifies the design and the
code that is developed. Functions designed using subtypes are shorter, because they
do not have a guard. Tests are simpler because throwing errors does not need to be
checked. Signatures are simpler because errors are not thrown. All this means that a
function is easier to understand, to refine, and to explain to others. Whenever possible
design using subtypes. In this textbook the version of move-alien developed using
the subtype posint is adopted.
To design the game-over? handler it is necessary to think about when this version
of the game comes to an end. The only way the game comes to an end is when the
alien reaches earth. In other words, the problem of detecting the end of the world
53 The game-over? Handler 219
is reduced to solving a problem about an alien. Think carefully about how can an
alien reaching earth be determined. Visually, the alien has reached earth when it is
at the bottom of scene. Now, think it terms of the alien data definition. How can it
be determined that an alien has reached the bottom of the scene? The alien can only
be at the bottom of the scene if its y coordinate is MAX-IMG-Y.
The above problem analysis allows specializing the template for functions on a
world as follows:
;; world → Boolean
;; Purpose: Detect if the game is over
(define (game-over? a-world)
(alien-reached-earth? (world-alien a-world)))
Running the code reveals that all the tests pass. Hooray! Allowing the game to run
until the alien reaches earth, however, reveals a small unexpected problem. The final
scene is displayed in Fig. 63a. The alien is not at the bottom of the scene. Frankly, it
does not look like it quite reached earth. It would be nicer if the final scene displayed
were the one in Fig. 63b.
This problem occurs because the universe teachpack stops drawing the world
when game-over? evaluates to #true. In the world displayed in Fig. 63a the alien
is about to move down and the direction is about to change to right. When this
move occurs, however, the universe teachpack detects the end of the game and
never draws that final world. To remedy this situation the universe API allows the
programmer to specify a function to draw the last world. This function is specified
in the stop-when stanza of the big-bang expression as follows:
54 Computing the Last Scene 221
; string → world
; Purpose: To run the game
(define (run a-name)
(big-bang INIT-WORLD
[on-draw draw-world]
[name a-name]
[on-key process-key]
[on-tick process-tick TICK-RATE]
[stop-when game-over? draw-last-world]))
The function draw-last-world must take as input a world and must return a scene.
To implement draw-last-world the template for functions on a world is special-
ized. The goal is to draw the given final world with a message stating that earth was
conquered. This message may be placed, for example, half away across and a quarter
of the way down in the scene using a font size of 36 and red letters. This analysis
yields the following specialization of the template for functions on a world:18
;; world → scene throws error
;; Purpose: To draw the game’s final scene
(define (draw-last-world a-world)
(if (= (posn-y (world-alien a-world)) MAX-IMG-Y)
(place-image (text "EARTH WAS CONQUERED!" 36 'red)
(/ E-SCENE-W 2)
(/ E-SCENE-H 4)
(draw-world a-world))
(error
(format "draw-last-world: Invalid world with ∼s as the
alien’s y coordinate. The alien’s y coordinate
must be in [∼s..∼s]."
(posn-y (world-alien a-world))
MIN-IMG-Y
MAX-IMG-Y))))
18
The string inputs to format should be typed in one line in DrRacket.
222 9 Aliens Attack Version 2
*** Ex. 97 — Carefully explain why the alien never goes off the scene.
55 What Have We Learned in This Chapter? 223
** Ex. 98 — An alternative ending for the game is the player saving earth if the
alien crashes into the rocket. Redesign game-over? and draw-last-world to
implement this alternative ending.
* Ex. 100 — The draw-last-world design did not develop a world subtype.
As a consequence, draw-last-world is a guarded function that may throw an
error. Redesign draw-last-world by developing a world subtype for worlds
that end the game.
Chapter 9 revealed that functions may consume instances of different data types. This
is the case of functions like move-alien and hit?. It turns out that it is common
for functions to consume different data types. Functions like move-alien and hit?
have a separate parameter for each data type. Now consider designing a function
that processes motorized vehicles like cars, motorcycles, and personal transporters
or a function that processes geometric shapes like squares, rectangles, triangles, and
ellipses. This is a situation that is quite different from designing move-alien and
hit?. Now, there is a single input: a motor vehicle. This input, however, may be an
instance of several different data types.
This chapter further explores how to design functions that consume data that may
vary. Given that there is variety in the data conditional expressions play a central
role. Remember that a conditional is needed to distinguish the different varieties.
The design may follow a bottom-up or a top-down approach. So far, a top-down
approach has been emphasized. In this chapter, a bottom-up approach is taken. Why
would you ever take a bottom-up approach? Sometimes it is easier to formulate the
simplest forms of data and then use them to define more complex data.
We shall explore the design of a function to process motor vehicles. The charac-
teristics of the different motor vehicles are:
Car Has a tank capacity in gallons, a miles per gallon, a maximum speed, and a
mode. The mode indicates if the car is running in economic mode. When the car
is running in economic mode, it may travel 20% further than in normal mode, but
the accelerator is less responsive.
Motorcycle Has a tank capacity in gallons, a miles per gallon, and a maximum
speed.
Personal Transporter Has a maximum miles per hour and maximum hours per
charge.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 225
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_10
226 10 Structures and Variety
56 A Bottom-Up Design
A bottom-up design starts with defining the different motor vehicles first. A car has
four characteristics and this suggests using a structure. A car and sample instances
of a car may be defined as follows:
#|
A car is a structure
(make-carr integer>=0 integer>=0 integer>=0 integer>=0 Boolean)
containing tank capacity in gallons, miles per gallon, maximum
speed, and economy mode flag.
;; carr . . . → . . .
;; Purpose: . . .
(define (f-on-carr a-carr . . .)
. . .(carr-gallons a-carr). . .(carr-mpg a-carr)
. . .(carr-maxspeed a-carr). . .(carr-mode a-carr))
;; Structure Definition
(define-struct carr (gallons mpg maxspeed mode))
selector functions, that may be useful to process a carr. Also given that there is no
variety in carr the template suggests at least one test using sample computations
and one test using sample values.
We now turn our attention to data analysis for motorcycles. An mc and sample
instances of an mc may be defined as follows:
#| A motorcycle, mc, is a structure
(make-mc integer>=0 integer>=0 integer>=0)
with a tank capacity in gallons, miles per gallon, and a
maximum speed.
;; Sample instances of mc
(define MC1 . . .)
;; mc . . . → . . .
;; Purpose: . . .
(define (f-on-mc an-mc . . .)
. . .(mc-gallons an-mc). . .(mc-mpg an-mc)
. . .(mc-maxspeed an-mc). . .)
;; Structure Definition
(define-struct mc (gallons mpg maxspeed))
;; Sample instances of mc
(define MC1 (make-mc 8 22 135))
(define MC2 (make-mc 10 20 145))
Observe that the defining an mc is similar to defining a carr. Useful expressions to
extract data from the given mc instance are found in the function definition template.
Given that there is no variety the function template suggests defining at least one
sample instance, one sample expression, one test using sample computations, and
one test using sample values. Two sample instances are defined for thorough testing
and to parallel the work done for carr.
228 10 Structures and Variety
The last motor vehicle to define is the personal transporter. An pt and sample
instances of an pt may be defined as follows:
#| A personal transporter, pt, is a structure
(make-pt integer>=0 integer>=0)
with a maximum miles per hour and a maximum hours per
charge.
;; Sample instances of pt
(define PT1 . . .)
;; pt . . . → . . .
;; Purpose: . . .
(define (f-on-pt a-pt . . .)
. . .(pt-mph a-pt). . .(pt-hpc a-pt))
;; Structure Definition
(define-struct pt (mph hpc))
;; Sample instances of pt
(define PT1 (make-pt 7 7))
(define PT2 (make-pt 6 10))
Observe that the data design steps are essentially the same as those taken for mc and
carr.
It is important to take stock of what has been achieved so far by following a
bottom-up approach. There are three data definitions with a function template and
sample instances for each. These data definitions correspond to the motor vehicle
subtypes. A bottom-up approach, therefore, first defines subtypes.
Once subtypes are defined, the next step is to define a union type. A union type
enumerates the type varieties that may be used to build instances. If A, B, and C are
types, then D may be defined as the union of A, B, and C. A, B, and C are subtypes of
D. We say that D is A’s, B’s, and C’s supertype. Functions written for a supertype must
be able to process instances of each of its subtypes. That is, a function on a supertype
56 A Bottom-Up Design 229
must be polymorphic. Polymorphic means that a function can process different types
of data.
In our example, a supertype for motor vehicles is needed. The supertype mv and
instances of mv may be defined as a union type:
#| A motor vehicle, mv, is either:
1. carr
2. mc
3. pt
;; Sample instances of mv
(define MVCARR1 . . .)
(define MVMC1 . . .)
(define MVPT1 . . .)
;; mv . . . → . . .
;; Purpose: . . .
(define (f-on-mv an-mv . . .)
(cond [(car? an-mv) (f-on-car an-mv . . .)]
[(car? an-mv) (f-on-mc an-mv . . .)]
[else (f-on-mv an-mv . . .)]))
;; Sample instances of mv
(define MVCARR1 CARR1)
(define MVCARR2 CARR2)
(define MVMC1 MC1)
(define MVMC2 MC2)
(define MVPT1 PT1)
(define MVPT2 PT2)
230 10 Structures and Variety
Observe that a union type is nothing more than a data type that has variety. You
have already learned about union types in previous chapters (e.g., speed in Chap. 5
and mn in Chap. 8) although they were not called union types. Therefore, it follows
that the definition template has a conditional to distinguish among the subtypes. As
expected, the function template also suggests defining at least one sample expression
and one test using sample computations for each subtype. Observe that the sample
instances are defined using the sample instances of mv’s subtypes. This can only be
done because an instance of a subtype is also an instance of the supertype. That is,
instances of carr, mc, and pt are instances of mv.
With data analysis, finished problems involving motor vehicles may be solved.
Consider writing a function to compute the maximum distance a motor vehicle
may travel. Following a bottom-up approach means that functions to compute the
maximum distance for each subtype (i.e., carr, mc, and pt) are written first. The
result of specializing the function templates for mc, pt, and carr is displayed,
respectively, in Figs. 65a and 66a. The specialization of the three templates varies in
one significant way. In Fig. 65a,b the functions return an integer greater than or equal
56 A Bottom-Up Design 231
(a) Maximum Distance for carr. (b) Maximum Distance for mv.
to zero. In Fig. 66a the function returns a number greater than or equal to zero. This
difference in the signatures arises from the observation that carr-maxdist may not
always return an integer. For example, (* 1.2 25 40) evaluates to 1131.6. All
three functions, however, are tested using check-expect. Using check-within
for carr-maxdist is not necessary, because (* 1.2 integer>=0 integer>=0)
is always an exact integer. A careful reader of the code may wonder about not
using check-within. To mitigate this type of concern an assumption statement
may accompany the function definition as follows:
;; carr → number>=0
;; Purpose: Return max distance the given carr may travel
;; on full
232 10 Structures and Variety
*** Ex. 101 — Change the data definition of a motor vehicle to include a boat:
A boat is either:
1. Cruise Boat that has two gas tanks in gallons, a
miles per gallon, and a mode. The mode indicates
if the boat is empty. When empty, a cruise boat
may travel 50 more miles per gallon than when not
empty.
2. Speed Boat that has a gas tank per gallon, a
miles per gallon, and a number of passengers: 1
or 2. When the boat has 2 passengers it travels
10 fewer miles per gallon than when it only has 1
passenger.
Refine mv-maxdist for the new motor vehicle definition.
*** Ex. 102 — Consider the following data definition:
A geometric shape is either:
1. square: Has a length
2. rectangle: Has a length and a width
3. circle: Has a radius
4. ellipse: Has a major axis length and a minor axis length
57 Code Refactoring 233
*** Ex. 103 — Design and implement a function to compute the perimeter of
a geometric shape as defined in the previous problem.
57 Code Refactoring
Code refactoring has significantly reduced the size of the code. The original
code’s four functions are reduced to two. Code refactoring plays a central role in
software development. It is used to improve design, structure, and efficiency without
changing software functionality.
** Ex. 105 — The expressions used to compute the maximum distance may
instead be refactored as follows:
(* (* 1.2 (carr-gallons a-carr)) (carr-mpg a-carr))
(* (carr-gallons a-carr) (carr-mpg a-carr))
(* (mc-gallons an-mc) (mc-mpg an-mc))
(* (pt-hpc a-pt) (pt-mph a-pt))
236 10 Structures and Variety
The computation of the maximum distances is always the product of two num-
bers. Use this code refactoring idea to refine the implementation of mv-maxdist
in Fig. 66b.
• When the subtype code is refactored, supertype sample expressions and functions
may have to be refined.
• Code refactoring is used to improve design, structure, and efficiency without
changing software functionality.
Chapter 11
Aliens Attack Version 3
Writing union types that contain structure-based subtypes endows us with the power
to refine Aliens Attack version 2 (from Chap. 9). This chapter discusses adding a
shot to the game. As in Aliens Attack version 2, the world in version 3 has a rocket,
an alien, and a direction. The world data definition is now refined to contain a shot.
We emphasize the singular here. There can be at most one shot in the game at any
time. A player may shoot only when there is no shot in the game. At the beginning
of the game, for example, the player may shoot. A player may not shoot again until
the last shot created goes off the top of the scene.
Now that a shot is part of the world it becomes possible for the player to win the
game. When does this happen? It is fairly straightforward to see that the player ought
to win when the alien is hit by the shot. It is less straightforward to precisely define
when an alien has been hit. Clearly, the alien has been hit when the coordinates of
the alien and the shot are the same. For example, if the alien and the shot are both (4
2) then the alien has been hit and the player wins the game. Now consider an alien
at (0 7), a shot at (0 6), and the direction being 'down. This puts the alien and
the shot at the left edge of the scene. The alien moves to coordinate (0 6) and the
shot moves to coordinates (0 7). Has the alien been hit? On the one hand, the alien
and the shot are never the same. On the other hand, the alien and the shot cross each
other. This is a situation that forces game designer to choose feature. If it is not a hit,
it makes the game a bit more challenging for the player to win. If it is a hit, it makes
the game a bit easier to win. The presented design chooses the former feature. An
alien moving down “sees” the shot coming and skittles beneath it to avoid the hit.
Adding a shot to the world means that world’s function template and world
sample instances must also be refined. In addition, any functions and tests that build
a world must be updated. This is the same process followed to develop Aliens Attack
2. The refinement presented follows a top-down design. The first step is to refine
the world data definition. The second step is to define a shot. The third step is
the refinement of the program and the design of new functions. Observe that data
analysis is performed first. Remember that if you do not have a clear understanding
of the data being processed, then it is impossible to design a solution to a problem.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 239
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_11
240 11 Aliens Attack Version 3
59 Data Definitions
Figure 69 displays a rendering of the game that now includes a shot. The only new
component is the shot. Therefore, the world data definition is refined as follows:
#| A world is a structure: (make-world rocket alien dir shot).
;; world . . . → . . .
;; Purpose: . . .
(define (f-on-world a-world . . .)
. . .(f-on-rocket (world-rocket a-world) . . .)
. . .(f-on-alien (world-alien a-world) . . .)
. . .(f-on-dir (world-dir a-world) . . .)
. . .(f-on-shot (world-shot a-world) . . .))
A shot always moves up. What changes when a shot moves up? Clearly, an image-y
coordinate is needed to represent a shot that moves up. Is an image-x coordinate
needed? As the shot moves up the image-x coordinate never changes. However, the
image-x coordinate may vary among shots. This coordinate is determined by the
position of the rocket when the shot is created. If the rocket is 4 when the player
shoots, then the new shot’s image-x coordinate is 4. If the rocket is 10 when the
player shoots, then the new shot’s image-x coordinate is 10. This means that an
image-x coordinate is needed. Using a posn to represent a shot is a good option.
What posn should be used for the shot at the beginning of the game? This is
an interesting question because the player has not created a shot. If a shot has not
been created, how can it have image-x and image-y coordinates? This situation is
suggesting that there is variety in shots: non-existing and existing shots. We have
already determined to represent an existing shot using a posn. The representation
choice for a non-existing shot is arbitrary. We shall define, NO-SHOT, a symbol
constant to represent such a shot. Based on this data analysis the data definition for
a shot and the template for a function on a shot are:
(define NO-SHOT 'no-shot)
#| A shot is either:
1. NO-SHOT
2. A structure (make-posn image-x image-y)
242 11 Aliens Attack Version 3
;; shot . . . → . . .
;; Purpose: . . .
(define (f-on-shot a-shot . . .)
(if (eq? a-shot NO-SHOT)
...
. . .(f-on-image-x (posn-x a-shot))
. . .(f-on-image-y (posn-y a-shot)). . .)))
Observe that a posn is used to represent both an alien and a shot. Although
the same representation is used, the expected behavior is quite different. A posn
representing a shot may only be moved up. On the other hand, a shot representing
an alien may be may be moved right, left, and down. This further highlights why
data definitions are important. They clearly outline that not all data represented using
a posn is the same.
Now that a shot is defined it is possible to define sample instances of a shot and
refined the sample instances of a world. These are:
;; Sample instances of shot
(define INIT-SHOT NO-SHOT)
(define SHOT2 (make-posn (/ MAX-CHARS-HORIZONTAL 2)
(/ (sub1 MAX-CHARS-VERTICAL) 2)))
(define SHOT3 (make-posn 4 MAX-IMG-Y))
(define SHOT4 (make-posn 14 MIN-IMG-Y))
In addition to rendering the rocket and the alien, the shot must be rendered. The
only decision that needs to be made is whether the alien or the shot is rendered
on top when a hit occurs. This is mostly a matter of personal preferences. In order
to clearly see that a hit has occurred, the chosen design renders the shot on top of
the alien. This means that the shot must be rendered in a scene that contains the
alien. Given that we have already developed code to render a scene that has the
alien and the rocket, all we need is function composition to draw the shot in
such a scene. First, let us refine the sample expressions for draw-world:
;; Sample expressions for draw-world
(define WORLD-SCN1
(draw-shot (world-shot INIT-WORLD)
(draw-alien (world-alien INIT-WORLD)
(draw-rocket
(world-rocket INIT-WORLD)
E-SCENE))))
(define WORLD-SCN2
(draw-shot (world-shot INIT-WORLD2)
(draw-alien (world-alien INIT-WORLD2)
(draw-rocket
(world-rocket INIT-WORLD2)
E-SCENE))))
Observe that the shot is rendered in a scene that contains the alien (and the rocket)
using function composition. In addition to such a scene, the new function to draw
the shot takes as input the world’s shot.
The above sample expressions lead to the following refinement of the function
definition for draw-world:
;; world → scene
;; Purpose: To draw the world in E-SCENE
244 11 Aliens Attack Version 3
)
(check-expect
(draw-world (make-world INIT-ROCKET INIT-ALIEN2
INIT-DIR2 SHOT2))
)
The next step is to design the auxiliary function to draw a shot. To do so, the
template for a function on a shot is specialized. Think carefully about how to draw a
shot. There are two varieties of shot. Think about each variety independently. If the
given shot is 'NO-SHOT, then there is nothing to draw in the given scene. If the given
shot is a posn, then the shot must be drawn at the image coordinates contained in
the shot. This problem analysis leads to the following template specialization:
;; shot scene → scene
;; Purpose: To draw the shot in the given scene
(define (draw-shot a-shot scn)
(if (eq? a-shot NO-SHOT)
scn
(draw-ci SHOT-IMG (posn-x a-shot) (posn-y a-shot) scn)))
60 The draw-world Refinement 245
)
The signature identifies a shot and a scene as input values and a scene as the output
value. The purpose statement concisely describes what the function does. The sample
expressions illustrate the computations that are performed for the different shot
varieties. The first sample expression illustrates that nothing needs to be computed for
INIT-SHOT (which is 'NO-SHOT). The second and third sample expressions illustrate
how to compute a scene that contains a shot using draw-ci (the same function used
to draw an alien a rocket). The conditional expression in the body of the function
definition first determines if the given shot is 'NO-SHOT. If so, the given scene
is returned without any further computation as done in the first sample expression.
Otherwise, the shot ci is rendered in the given scene at the image coordinates
contained in the given shot as done in the other sample expressions. The tests using
sample computations employ the differences among the sample expressions and the
defined constants, as before, to illustrate that the defined function returns the same
values as those obtained from the sample expressions. Finally, the tests using sample
values further illustrate that the function works for each variety of shot using a
combination of input values not used in the sample expressions.
246 11 Aliens Attack Version 3
The key event handler must be updated to construct worlds that contain a shot. In
addition, this function must also process the key event associated with shooting. The
key choice is arbitrary, but as indicated before a popular choice is to use the space
key to allow a player to shoot.19 This means that the key data definition and the
template for a function on a key must be refined. This is done as follows:
A key is either:
1. "right"
2. "left"
3. " "
4. not "right", "left", or " "
;; key . . . → . . .
;; Purpose: . . .
(define (f-on-key a-key . . .)
(cond [(key=? a-key "right") . . .]
[(key=? a-key "left") . . .]
[(key=? a-key " ") . . .]
[else . . .]))
The data definition now contains four key varieties. Therefore, there are now four
sample instances, four sample expressions, four stanzas in the definition-template’s
conditional, and four tests using sample computations. The refined template clearly
informs us that there must be at least 4, not 3, of each of these items in the definition
of process-key.
Next, analyze the problem of processing the space key. When should a shot be
created? There can only be at most one shot in a world. This means a new shot
cannot be created every time the player tries to shoot. A decision must be made as
to whether to construct a new shot or not. The details of how this is done are left to
an auxiliary function, but there are two cases. If there is no shot in the game, then
a new shot is created and added to the world. Otherwise, a new shot is not created
and the shot in the world is left unchanged.
Start by refining the sample instances. It is logical to start with this refinement
because these constants may be used to simplify sample expression refinement and
development. The four needed sample instances may be defined as follows:
;; Sample instances of key
(define KEY1 "right")
(define KEY2 "left")
(define KEY3 "m")
(define KEY4 " ")
Next, refine the sample expressions for process-key. Recall that the new world
data definition tells us that worlds must be constructed with a shot and that the
template for a function on a key tells us there must be at least four sample expressions.
The sample expressions may be refined as follows:
;; Sample expressions for process-key
(define KEY-RVAL (make-world (move-rckt-right
(world-rocket INIT-WORLD))
(world-alien INIT-WORLD)
(world-dir INIT-WORLD)
(world-shot INIT-WORLD)))
(define KEY-LVAL (make-world (move-rckt-left
(world-rocket INIT-WORLD))
(world-alien INIT-WORLD)
(world-dir INIT-WORLD)
(world-shot INIT-WORLD)))
(define KEY-SVAL (make-world (world-rocket INIT-WORLD)
(world-alien INIT-WORLD)
(world-dir INIT-WORLD)
(process-shooting
(world-shot INIT-WORLD)
(world-rocket INIT-WORLD))))
(define KEY-SVAL2 (make-world (world-rocket INIT-WORLD2)
(world-alien INIT-WORLD2)
(world-dir INIT-WORLD2)
248 11 Aliens Attack Version 3
(process-shooting
(world-shot INIT-WORLD2)
(world-rocket INIT-WORLD2))))
(define KEY-OVAL INIT-WORLD2)
The sample expressions for "right" and "left", KEY-RVAL and KEY-LVAL, are
refined to build a world with an unchanged shot. The sample expression for the fourth
key variety, KEY-OVAL, remains unchanged because a world is not constructed. The
remaining sample expressions, KEY-SVAL and KEY-SVAL2, illustrate how a new
world is computed, respectively, when a world does not contain a shot and when
it contains a shot. In either case, as per the problem analysis above, the processing
of the shooting key is left to an auxiliary function. This function requires at least two
inputs: the world’s shot and the world’s rocket. The shot is needed to determine
if a new shot may be created. The rocket is needed to provide the image-x
coordinate if a new shot is created. If any further inputs are needed by this function
its design ought to reveal them.
Abstracting over the sample expressions confirms that process-key must be
refined to have a conditional with four stanzas. The refined function is:
;; world key → world
;; Purpose: Process a key event to return next world
(define (process-key a-world a-key)
(cond [(key=? a-key "right")
(make-world (move-rckt-right (world-rocket a-world))
(world-alien a-world)
(world-dir a-world)
(world-shot a-world))]
[(key=? a-key "left")
(make-world (move-rckt-left (world-rocket a-world))
(world-alien a-world)
(world-dir a-world)
(world-shot a-world))]
[(key=? a-key " ")
(make-world (world-rocket a-world)
(world-alien a-world)
(world-dir a-world)
(process-shooting
(world-shot a-world)
(world-rocket a-world)))]
[else a-world]))
Observe that there is now a stanza for the new key variety (i.e., " ") and that world
construction always includes a shot.
Tests using sample computations are refined by adding tests using KEY-SVAL and
KEY-SVAL2:
(check-expect (process-key INIT-WORLD " ") KEY-SVAL)
(check-expect (process-key INIT-WORLD2 " ") KEY-SVAL2)
61 The process-key Refinement 249
The other tests using sample computations remain unchanged. Tests using sample
values are updated to correctly construct worlds and to further illustrate how the
space key is processed. For example, the following are refined tests using sample
values:
;; Tests using sample values for process-key
(check-expect (process-key (make-world
(sub1 MAX-CHARS-HORIZONTAL)
INIT-ALIEN
'right
INIT-SHOT)
"right")
(make-world (sub1 MAX-CHARS-HORIZONTAL)
INIT-ALIEN
'right
INIT-SHOT))
* Ex. 107 — Refine all the process-key tests using sample values that are in
Aliens Attack 2.
* Ex. 108 — Add tests using sample values to further illustrate how the space
key is processed.
* Ex. 109 — There are repetitions in the process-key tests using sample
values. For example, the first test above creates the same world twice. Rewrite
the tests without repetitions. Make sure not to change what is being tested.
With the refinement of process-key completed focus turns to the design of the
auxiliary function to process the space key. This function takes as input two different
types of data: a shot and a rocket. Which template ought to be used to design
this function? To answer this question it is necessary to do careful problem analysis.
This function must create a new shot only when the game does not already have a
posn shot. When ought a new shot be created? A new shot is created only when
the game’s shot is 'NO-SHOT. This immediately suggests designing this function
by specializing the template for a function on a shot because its body distinguishes
among shot varieties.
250 11 Aliens Attack Version 3
To refine process-tick, like any other problem, first perform problem analysis.
Outline what needs to happen when the clock ticks. In Aliens Attack 2 tick processing
created a new world by moving the alien and computing a direction. These tasks
must still be performed in this new Aliens Attack version. To construct a new world,
however, a shot instance is needed. Does a shot change after a clock tick? Once
again, reason about each shot variety. If the shot is 'NO-SHOT, then it does not
change. If the shot is a posn, then it either must move up or disappear because it has
reached the top of the scene. The details of how the new shot is computed is left
to an auxiliary function that processes a shot.
The sample expressions must now illustrate that the alien is moved, a new direction
is computed, and the shot is moved (if possible). The sample expressions are refined
as follows:
;; Sample expressions for process-tick
(define AFTER-TICK-WORLD1
(make-world
(world-rocket INIT-WORLD)
(move-alien (world-alien INIT-WORLD)
(world-dir INIT-WORLD))
(new-dir-after-tick (move-alien
(world-alien INIT-WORLD)
(world-dir INIT-WORLD))
(world-dir INIT-WORLD))
(move-shot (world-shot INIT-WORLD))))
(define AFTER-TICK-WORLD2
(make-world
(world-rocket INIT-WORLD2)
(move-alien (world-alien INIT-WORLD2)
(world-dir INIT-WORLD2))
(new-dir-after-tick (move-alien
(world-alien INIT-WORLD2)
(world-dir INIT-WORLD2))
(world-dir INIT-WORLD2))
(move-shot (world-shot INIT-WORLD2))))
Observe that, just as in Aliens Attack 2, the rocket is unchanged, the alien is moved,
and a new direction is computed. Moving the shot is left to an auxiliary function,
move-shot, that is to be designed after completing the process-tick refinement.
252 11 Aliens Attack Version 3
(check-expect (process-tick
(make-world
INIT-ROCKET2
(make-posn (- MAX-CHARS-HORIZONTAL 2) 10)
'right
SHOT2))
(make-world INIT-ROCKET2
(make-posn MAX-IMG-X 10)
'down
(move-shot SHOT2)))
62 The process-tick Refinement 253
Writing tests for process-tick has provided an insight into how to move a shot: a
posn shot is moved by decreasing its image-y coordinate. It is still necessary to
perform problem analysis. There are two shot varieties. How is a 'NO-SHOT shot
moved? If there is no shot in the game, then there is nothing to move. Therefore, the
shot remains unchanged. Now, carefully think about how a posn shot is moved.
Does a shot always move up? If it did, then the player would only be able to
shoot once during the game. This, however, is not what is needed. We wish to
allow the player to shoot again when a shot goes off the top of the scene. How
does this affect how a shot is moved? In order to allow the player to shoot again
a posn shot must become a 'NO-SHOT shot when it moves off the top of the
scene. This analysis suggests that there are three conditions that must be tested:
(eq? a-shot NO-SHOT) → shot remains unchanged
(= (posn-y a-shot) MIN-IMG-Y) → shot changes to 'NO-SHOT
(not (= (posn-y a-shot) MIN-IMG-Y)) → shot’s image-y is decreased
Armed with the insights provided by problem analysis, the template for functions
on a shot is specialized. We start with sample expressions. Given that there are three
conditions at least 3 sample expressions need to be defined:
254 11 Aliens Attack Version 3
* Ex. 110 — The design of move-shot may more strictly adhere to the tem-
plate for a function on a shot by not changing the number of conditions from
2 to 3. The following is an alternative design using the conditional to strictly
discriminate among the shot varieties:
;; shot → shot
;; Purpose: To move the given shot
(define (move-shot a-shot)
(cond [(eq? a-shot NO-SHOT) a-shot]
[else
(cond [(= (posn-y a-shot) MIN-IMG-Y) NO-SHOT]
[else (make-posn
256 11 Aliens Attack Version 3
(posn-x a-shot)
(move-up-image-y (posn-y a-shot)))]))
This design clearly communicates how each shot variety is processed. The
processing of a posn shot requires a conditional expression. Is this design
easier to understand? Do you prefer this design or the design that does not have
a nested conditional expression?
By now, it is likely clear that the game may end in two ways. The first, as before,
occurs when the alien reaches earth and the player loses. The second occurs when
the alien is hit by a shot and the player wins.
The above problem analysis suggests that at least three sample expressions are
needed. One is needed to illustrate when the game is not over. Two are needed to
illustrate when the game ends. One for when the player loses and one for when the
player wins. For example, the following are sample expressions that satisfy these
constraints:
;; Sample expressions for game-over?
(define GAME-OVER1 (or (alien-reached-earth?
(world-alien INIT-WORLD2))
(hit? (world-shot INIT-WORLD2)
(world-alien INIT-WORLD2))))
We must now analyze how to determine if a given alien is hit by a given shot. A
'NO-SHOT shot can never hit an alien. This means that the given shot must be
a posn shot to have a hit. If the given shot is a posn shot, then its image-x
and image-y coordinates must be the same as those of the given alien. Observe
258 11 Aliens Attack Version 3
that nothing in this problem analysis suggests that a decision must be made. Thus,
a conditional is not needed. Given this observation, we proceed by specializing the
template for a function on an alien (and not the template for a function on a shot
that contains a conditional).
We start by writing sample expressions for hit?. The idea is to write at least
two sample expressions: one that returns #false and one that returns #true. The
following are two such sample expressions:
(define ALIEN3 (make-posn 4 MAX-IMG-Y))
The computation of the last scene must also be refined. Now, the player may win
and, if so, a winning message ought to be displayed. To start, it is best to define a
sample final world representing a player win such as:
(define FWORLD3 (make-world 7 (make-posn 19 3)
'left (make-posn 19 3)))
Now, think carefully. How do we decide to display a winning or a losing message?
How do we know that FWORLD1 and FWORLD2 represent a loss for the player and
FWORLD3 represents a win for the player? As before, it is a loss if the image-y of the
alien is MAX-IMG-Y. It is a win if the alien is hit.
The sample expression using FWORLD3 is:
(define FWORLD3-VAL (place-image
(text "EARTH WAS SAVED!" 36 'green)
(/ E-SCENE-W 2)
(/ E-SCENE-H 4)
(draw-world FWORLD3)))
Observe that the message is different from the other two sample expressions in Aliens
Attack 2. The development of a third sample expression means that a test using this
new sample value must be added:
(check-expect (draw-last-world FWORLD3) FWORLD3-VAL)
The other tests using sample values remain unchanged. The test using a sample value
is changed to build a world that contains a shot. It is still used to test the error
thrown. The refined test is:
;; Tests using sample values for draw-last-world
(check-error
(draw-last-world (make-world 10
(make-posn 7 20)
'right
(make-posn 2 3)))
"draw-last-world: Invalid world with #(struct:posn 7 20)
as the alien value and #(struct:posn 2 3) as the shot value.")
Observe that the world constructed does not represent a game-ending world and,
thus, is expected to force draw-last-world to throw an error.
260 11 Aliens Attack Version 3
arbitrary size just as a grocery list. Thus, we may consider using a list to represent
the aliens and the shots.
Given that many different types of data are naturally represented as lists, ISL+
provides support for lists. The smallest possible list is the empty list. In ISL+, the
empty list is represented as '(). We can use the empty list to define many different
types of list. For example, an empty grocery list, an empty list of aliens, and an
empty list of shots may be defined as follows:
(define E-GLIST '())
(define E-LOA '())
(define E-LOS '())
Extracting the second, third, fourth, and so on up to the eighth element of a list
is common enough that ISL+ provides functions to do so directly. Not surprisingly,
these functions are called second, third, fourth, and so on up to eighth. Be
mindful when using these functions because if given a list that is too short they
throw an error. These function provide a useful shorthand notation to access list
elements up to the eighth. From the ninth on you must write your own function.
66 Shorthand for Building Lists 269
Consider constructing a list of first 7 digits. Your code may look something like this:
(define SEVEN-DIGITS
(cons
0
(cons 1
(cons 2
(cons 3
(cons 4
(cons 5
(cons 6 empty))))))))
(check-expect (first SEVEN-DIGITS) 0)
(check-expect (second SEVEN-DIGITS) 1)
(check-expect (third SEVEN-DIGITS) 2)
(check-expect (fourth SEVEN-DIGITS) 3)
(check-expect (fifth SEVEN-DIGITS) 4)
(check-expect (sixth SEVEN-DIGITS) 5)
(check-expect (seventh SEVEN-DIGITS) 6)
Observe that there is a lot of repetition in the code to construct the list. Specifically,
there is an application of cons for every element of the list. This may not be too
cumbersome for a list with seven elements, but what if you are now asked to define
a list with the integers in [0..19]? You would need to write cons twenty times. To
avoid all this repetition ISL+ provides three shorthand constructors for lists.
The first is used when the elements of the list are known in advance. A quoted
list has the elements listed inside parenthesis preceded by a '. For example, we may
refactor the SEVEN-DIGITS definition as follows:
(define SEVEN-DIGITS '(0 1 2 3 4 5 6))
We have explored how to create and access lists. As part of our exploration, we have
discovered that lists may be used to store different types of data. We have not yet
explored how to process a list. To do so we first need to be able to develop a data
definition and a function template for a given type of list. Consider defining a list of
numbers. Let us look at some examples based on our knowledge of lists so far:
'()
(cons 87 '())
(cons 24 (cons 87 '()))
(cons 16 (cons 24 (cons 87 '())))
(cons 31 (cons 16 (cons 24 (cons 87 '()))))
If you think about it carefully, you can see a distinct pattern. There are two list-of-
numbers varieties. The first is the empty list of numbers. The second is a non-empty
list of numbers constructed using cons. This immediately suggests that to define a
list of numbers, we need a data definition with two varieties:
;; A list of numbers (lon) is either
;; 1. '()
;; 2. (cons ??? ???)
Now, we must be precise about the types of the arguments to cons. Given that we
are defining a list of numbers, it is clear that the first argument must be a number.
This takes us a step closer to the needed data definition:
272 12 Lists
are useful to define data of arbitrary size like lists. In fact, any data of arbitrary size
requires a recursive data definition. So, why are students steered away from recursive
data definitions? This is likely rooted in the fact that recursive data definitions may be
nonsense. For example, someone may attempt to define a list of numbers as follows:
; A list of numbers (lon2) is a (cons number lon2)
Is this a useful data definition? Let us try to derive '(cons 24 (cons 87 '())):
lon2 → (cons 24 lon2) substitute lon2 using rule 2
→ (cons 24
(cons 87 lon2)) substitute lon2 using rule 2
The derivation fails because we are unable to instantiate the lon2 in (cons 87
lon2). You may say that clearly it should be '(). In fact, this would be wrong.
Nowhere in this data definition does it say that '() is a lon2. It is important to be
precise with our data definitions. Otherwise, we will be unable to solve problems.
This leads to asking ourselves what constitutes a useful recursive data definition. In
order to be useful, a recursive data definition must have the following characteristics:
1. At least two subtypes (varieties)
2. At least one subtype that does not contain a selfreference
3. At least one subtype that contains a selfreference
The subtypes that do not contain a selfreference are known as base subtypes.
These are concrete values that are known to be instances of the data type defined.
These concrete values break the circularity in a derivation. The subtypes that do
contain a selfreference are known as recursive subtypes. These are the varieties
that introduce circularity which endows us with the power to define data of arbitrary
size. In the recursive data definition for lon, we have '() as a base subtype and
(cons number lon) as a recursive subtype. The recursive data definition for lon2
is not useful because it does not have a base subtype.
With our newly acquired knowledge, we can now define a representation for
multiple aliens and multiple shots in Aliens Attack. Given that both are data of
arbitrary size, we need a recursive data definition for each. At this point, the only
recursive data type we know is list. Thus, let us try to use a list to define an arbitrary
number of aliens. Remember that it must have the three characteristic above required
for a useful recursive data definition. A list of aliens may be defined as follows:
;; A list of alien (loa) is either:
;; 1. '()
;; 2. (cons alien loa)
* Ex. 118 — Create a data definition and sample instances for a grocery list.
*** Ex. 120 — Create a data definition for a composed image. A composed
image may contain one or more rectangles and circles that are above, next, or
overlaid in relation to each other.
The next natural step is to develop a function template for lon, loa, and los.
However, notice that the three data definitions are almost identical. In other words,
there is a lot of repetition among them. When we have repetition among expressions,
an abstraction step introduces a variable to obtain a function. Can we apply an
abstraction step to avoid repetitions among data definitions?
The situation we face is a bit different from abstraction over expressions. When
you abstract over expressions, a difference is always a value and a variable is used
to represent the value. This felt quite natural because you are familiar with variables
from your Mathematics courses. The difference among the data definitions for lon,
loa, and los is a type. For lon the type used is number. For loa the type used
69 Function Templates for Lists 275
is alien. For los the type used is shot. When abstracting over data definitions
the variables used to capture the differences are type variables. Type variables
represent types not instances of types (i.e., values). A type variable is needed for
each difference.
Let X be the single difference among lon, loa, and los. Instead of using a
concrete type (like number, alien, or shot), we write a data definition using X:
;; A list of X ((listof X)) is either:
;; 1. '()
;; 2. (cons X (listof X))
The data definition is recursive just like the data definitions for lon, loa, and los.
Observe that if we plug in number for X we obtain the data definition for lon.
Similarly, plugging in alien and shot for X yields, respectively, the data definitions
for loa and los. The notation (listof X) is used to emphasize that a type must be
plugged in to obtain a concrete data definition. Clearly, this data definition works for
many different types. A data definition that works for many different types is called
a generic (or parameterized) data definition.
As we shall see, a generic data definition may be used in two ways in our signatures.
We may substitute X with a concrete type to specify a concrete data definition or we
may use (listof X) directly. We shall start with examples of the former. The use
of the latter will follow after we learn how to abstract over functions.
We can write a function template for a (listof X) and use it to define the templates
for lon, loa, and los. Before that, however, we must learn to write a function
template for a recursive data definition. Using the knowledge you have accumulated
so far, we can begin to develop the function template for (listof X):
;; Sample instances of (listof X)
(define LOX1 . . .)
(define LOX2 . . .)
;; (listof X) . . . → . . .
;; Purpose: . . .
(define (f-on-loX a-loX . . .)
(if (empty? a-loX)
...
. . .))
;; (listof X) . . . → . . .
;; Purpose: . . .
(define (f-on-loX a-loX . . .)
(if (empty? a-loX)
...
. . .(f-on-X (first a-loX)). . .
. . .(f-on-loX (rest a-loX) . . .). . .))
We can use the generic data definition for a (listof X) to define a list of numbers:
A lon is a (listof number)
Let us take a close look at what this means. Plugging in number for X in the generic
data definition for (listof X) yields:
;; A list of number (lon) is either
;; 1. '()
;; 2. (cons number lon)
This is exactly the data definition derived above. Plugging in number for X in the
function template for a function on a (listof X) yields a concrete function template
for a lon:
;; Sample instances of lon
(define LON1 . . .)
(define LON2 . . .)
...
278 12 Lists
;; lon . . . → . . .
;; Purpose: . . .
(define (f-on-lon a-lon . . .)
(if (empty? a-lon)
...
. . .(f-on-number (first a-lon)). . .
. . .(f-on-lon (rest a-lon) . . .). . .))
;; lon → lon
;; Purpose: Return a list of the squares of the given lon
(define (square-lon a-lon)
(if (empty? a-lon)
'()
(cons (sqr (first a-lon))
(square-lon (rest a-lon)))))
*** Ex. 122 — Design a function that returns a list of the lengths of all the
strings in a list of strings. Make sure you have a data definition for all the list
data types needed.
*** Ex. 124 — For the previous exercise, do you need a conditional to de-
fine the predicate as suggested by the data definition for (listof X)? If you
used a conditional for the previous exercise refactor your solution to not use a
conditional.
• The constructor for a non-empty list, cons, builds a new list by adding an element
to the front of an existing list.
• The selectors for lists are first and rest.
• When using an error-throwing function, we did not write use check-error to
test that an error is thrown.
• A quoted list may be used when all the list values are known and can be enumer-
ated.
• The function list may be used when the expressions for each list value are
known and can be enumerated.
• A quasiquoted list may be used when for the list elements either a value or an
expression for the value is known. Expressions that need to be evaluated must be
preceded by a comma.
• A recursive data definition is needed to define data of arbitrary size like lists.
• A recursive data definition always has a variety of subtypes of which at least one
is a base subtype and at least one is a recursive subtype.
• A recursive subtype contains a selfreference to its supertype.
• A generic data definition is an abstraction over types and may be used to define
many different concrete data types.
• For (listof X) there are two varieties: '() is the base subtype and (cons X
(listof X)) is the recursive subtype.
• A generic data definition leads to a generic function template.
• In a function template for data of arbitrary size, the definition template has a
recursive call for every selfreference in the data it is designed to process.
• Data of arbitrary size is processed using a recursive function.
• Recursion based on the structure of the data is called structural recursion.
• Structural recursion implements a divide and conquer design.
• Proper use of structural recursion guarantees that functions are not infinite recur-
sions.
• A concrete type and a concrete function template is obtained from a generic type
and its generic template by plugging in a type for each type variable.
• A concrete type and its concrete template are used to design functions. For
example, (listof number) and its function template are used to design any
function that processes a list of numbers.
Chapter 13
List Processing
72 List Summarizing
As a student, you are likely to keep track of your quiz grades in a class throughout
the semester. Why would you want to do this? Perhaps, your quiz average is 15% of
your final grade. Therefore, you would like to compute your quiz average every time
you earn a new quiz grade. How can you represent your quiz grades? Ask yourself if
you know the number of quizzes in advance. The most likely answer is that you do
not. After all, professors are likely to have pop quizzes or not announce the number
of quizzes in advance. Therefore, they may be, for example, 0, 19, or 7 quizzes.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 281
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_13
282 13 List Processing
The point is that the number of quizzes is arbitrary and must be represented using
compound data of arbitrary size. Thus, a structure is a poor choice to represent the
quiz grades, and a list of numbers is a much better representation.
A first attempt to define a list of quiz grades may be:
A list of quiz grades (loq) is a (listof number).
Is this a reasonable representation of quiz grades? Unfortunately, it is not. Given this
data definition, the following is a valid list of quiz grades:
'(87 65 92 -45 88 -7 98)
Clearly, this is a list of numbers. Is it a list of quiz grades? Clearly, it is not because
a quiz grade cannot be negative. We need to refine our data definition to be more
specific about what a quiz grade is. Assuming a quiz grade is based on a 100-point
scale, we may say:
;; A quiz grade (qg) is a number in [0..100]
;; loq . . . → . . .
;; Purpose: . . .
(define (f-on-loq a-loq . . .)
(if (empty? loq)
'()
. . .(f-on-qg (first a-loq)). . .
. . .(f-on-loq (rest a-loq) . . .). . .))))
* Ex. 125 — Define more sample instances of loq and more tests using sample
expressions for avg-log.
*** Ex. 126 — Develop a data definition for a non-empty (listof X). Re-
design avg-loq around processing a non-empty (listof qg).
We now proceed to design the auxiliary functions for the sum and the length of
a loq. The sum of an empty loq is 0. The sum of a non-empty loq is the sum of
the rest of the given loq and the first element of the given loq. Observe that we
statically reason about the loq. That is, we reason about what to do with the needed
value obtained from the first quiz grade and the needed value obtained from the rest
of the list. We do not dynamically reason about the loq. That is, we do not say that
we add the first element of the list, the second element of the list, the third element
of the list, and so on. We do not because it is not always clear what “and so on”
means. Furthermore, static reasoning is easily translated to a solution expressed as
a program. That is, it is clear that two things are needed to be done: process the
first element and process the rest of the given list. This problem analysis leads to the
following specialization of the template for a function on a loq:
;; Sample values of loq
(define LOQ3 '(50 90 80 60 70))
;; loq → number
;; Purpose: To compute the sum of the given loq
(define (sum-loq a-loq)
(if (empty? a-loq)
0
(+ (first a-loq) (sum-loq (rest a-loq)))))
An extra sample loq instance is defined to improve the illustration of the abstraction
that leads to the function definition. Observe that it is easy to see that the code
implements the strategy suggested by static reasoning.
The final task is to design a function to compute the length of a loq. The length
is 0 if the given loq is empty. What is the answer if the loq is not empty? Use static
reasoning to determine the solution. According to the template for a function on a
loq, you need a value obtained from the first qg, a value obtained from the rest of the
qgs, and a function to combine these two values. Ask yourself, what does the first qg
contribute to the length? It contributes 1 to the length of the given loq. What does
the rest of the quiz grades contribute to the length? It contributes its length. How do
we combine these two numbers to obtain the length of the given loq? The length
of the given loq is obtained by adding these two numbers. This problem analysis
yields the following template specialization:
;; loq → number
;; Purpose: To compute the length of the given loq
(define (length-loq a-loq)
(if (empty? a-loq)
0
(+ 1 (length-loq (rest a-loq)))))
* Ex. 127 — Design and implement a function to compute the average of a list
of numbers.
** Ex. 128 — Design and implement a function to compute the product of a
list of numbers.
286 13 List Processing
* Ex. 129 — Design and implement a function to count the number of aliens
in a (listof alien).
** Ex. 130 — Design and implement a function to append the strings in a list
of strings.
73 List Searching
Another common operation is searching a given list for a value. For example, given
a list of numbers, a-lon, and a number, x, you need the sublist of a-lon that starts
with the first occurrence of x. We start with problem analysis. If a-lon is empty
the answer is '() because a-lon does not contain x. What if the list is not empty?
Think about where x may be in terms of the structure of a-lon. We observe that
x may be the first element of the list. In this case, the answer is a-lon given that
(first a-lon) is the first occurrence of x. It is also possible that (first a-lon)
is not equal to x. In this case, (rest a-lon) must be searched. This, of course, is
done recursively as suggested by the template for a function on an lon. Will this
design idea work if x is not in a-lon? If you think carefully about it you realize
that it does. When x is in (rest a-lon), the recursive search stops when x equals
(first a-lon). When x is not in (rest a-lon), the recursive search stops when
a-lon equals '() and returns '()—the correct answer. Finally, observe that there
are three conditions that need to be detected (not two as suggested by the template
for a function on a lon).
We now need to develop sample expressions using sample lon instances to
illustrate how the result is computed for each of the three conditions. We define the
following sample lon instances:
;; Sample instances of lon
(define LON1 '())
(define LON2 '(1 2 3 4))
Using these sample instances, we define the following sample expressions:
;; Sample expressions for xsublist-lon
(define XSUBLIST-LON1-VAL '())
(define XSUBLIST-LON2-VAL1 LON2)
(define XSUBLIST-LON2-VAL2 (xsublist-lon 3 (rest LON2)))
(define XSUBLIST-LON2-VAL3 (xsublist-lon 0 (rest LON2)))
The first sample expression illustrates the answer when searching for, say, 7 in the
empty lon. The second sample expression illustrates the answer when searching for
1 in an lon that has 1 as its first number. The third and fourth sample expressions
illustrate how to compute the answer when the given number (3 and 0, respectively)
is not the first number in the given lon. Observe that the third sample expression
73 List Searching 287
illustrates searching for a number that is in the rest of the given list while the fourth
sample expression searches for a value not contained in the rest of the list. These
last two sample expressions illustrate the expression needed to make a recursive call
to search the rest of the given lon–just as suggested by the function template for a
function on a lon.
We can now finalize the specialization of the function template as follows:
;; number lon → lon
;; Purpose: To return the sublist of the given lon that
;; starts with the first instance of the given
;; number.
(define (xsublist-lon x a-lon)
(cond [(empty? a-lon) '()]
[(equal? x (first a-lon)) a-lon]
[else (xsublist-lon x (rest a-lon))]))
* Ex. 131 — Design and implement a function that returns the sublist of a
given (listof string) that starts with the first instance of a string that has a
length less than 5.
*** Ex. 132 — Design and implement a function that returns the first instance
of an even number in a given (listof number). If there are no even numbers
in the given (listof number), the function returns #false. For this problem,
you need a data definition for the function’s result type.
**** Ex. 133 — A list of alphabet symbols is a list that only contains the
symbols either in ['a..'z] or in ['A..'Z]. Design and implement a function
that returns the first instance of a symbol in ['a..'z] in a given list of alphabet
symbols. Make sure to follow all the steps of the design recipe. You need to
develop two data definitions to solve this problem.
288 13 List Processing
74 List ORing
A variant of list searching is list ORing. ORing determines if there exists a list value
in a given list, L, that satisfies some property P. That is, we seek to determine if any
element in L satisfies P. Think about the varieties of L. What is the answer if L is
empty? The answer may not be clear to you. That is fine. Let us delay the answer for
now and think about a list of length 1 whose only element, p, does not satisfy P. The
list looks as follows:
L = (cons p '())
Is there an element in L that satisfies P? Clearly the answer is no. Now, think
about how L is processed. You need to apply P to p to obtain #false. This result
must be ored with the result obtained from the rest of L. The rest of L is always
recursively processed. This means that '() must be recursively processed. The only
way processing (cons p '()) returns #false is if processing '() returns #false.
This means that the answer is #false when L is empty. If L is not empty, then you
must apply or to the result of applying P to p and of recursively processing the rest
of L.
For example, in Aliens Attack with multiple aliens all the aliens move in the same
direction. The direction only changes when there exists an alien that is either at the
left or the right edge of the scene. This means that we need predicates to determine
if there exists an alien located at either the right or left edge in a list of aliens.
Carefully think about what it means for there to exist an alien that is at the left edge
of the scene. One condition is that the given list of aliens cannot be empty. This is
a necessary condition for such an alien to exist, but it is not sufficient. A necessary
condition is one that must be true for a predicate to be true, but is not enough to
conclude that the predicate is true. A sufficient condition is one that allows us to
conclude that the predicate is true. A list of aliens must not be empty for there to
be an alien at the left edge of the scene. A list of aliens that is not empty is not
sufficient, because it may not have an alien at the left edge. This means we need
more conditions to hold in order to conclude that there exists an alien at the left edge.
What other condition(s) must hold? Once again, think in terms of the structure of
the list you need to process. Either the first alien is at the left edge or an alien in the
rest of the list of aliens is at the left edge. Observe that no decision must be made to
compute this value. That is, a conditional as suggested by the template for a function
on a list of aliens is not needed. Instead, we can use and to detect if both conditions
hold. We can use or to detect if the first alien is at the left edge or if any alien in the
rest of the list is at the left edge.
74 List ORing 289
Based on the above problem analysis, we can specialize the template for a function
on a loa as follows:
(define ALIEN-8-0 (make-posn 8 0))
;; loa → Boolean
;; Purpose: To determine if any alien is at scene’s left edge
(define (any-alien-at-left-edge? a-loa)
(and (not (empty? a-loa))
(or (alien-at-left-edge? (first a-loa))
(any-alien-at-left-edge? (rest a-loa)))))
(define LEDGE-INIT-LOA-VAL
(and (not (empty? INIT-LOA))
(or (alien-at-left-edge? (first INIT-LOA))
(any-alien-at-left-edge? (rest INIT-LOA)))))
(define LEDGE-LOA-VAL
(and (not (empty? EDGE-LOA))
(or (alien-at-left-edge? (first EDGE-LOA))
(any-alien-at-left-edge? (rest EDGE-LOA)))))
(define LEDGE-LOA2-VAL
(and (not (empty? EDGE-LOA2))
(or (alien-at-left-edge? (first EDGE-LOA2))
(any-alien-at-left-edge? (rest EDGE-LOA2)))))
Naturally, the next task is to design a function to determine if any alien is at the
right edge. Its design is similar to any-alien-at-left-edge?’s design. The
sufficient condition tests if an alien is at the right edge instead of the left edge.
That is, a given loa has an alien at the right edge if the loa is not empty
and either the first alien is at the left edge or an alien in the rest of the loa
is at the left edge. The specialization of the template for functions on a loa
yields:
;; loa → Boolean
;; Purpose: To determine if any alien is at scene’s right edge
(define (any-alien-at-right-edge? a-loa)
(and (not (empty? a-loa))
(or (alien-at-right-edge? (first a-loa))
(any-alien-at-right-edge? (rest a-loa)))))
(define EDGE-INIT-LOA-VAL
(and (not (empty? INIT-LOA))
(or (alien-at-right-edge? (first INIT-LOA))
(any-alien-at-right-edge? (rest INIT-LOA)))))
(define EDGE-LOA-VAL
(and (not (empty? EDGE-LOA))
(or (alien-at-right-edge? (first EDGE-LOA))
(any-alien-at-right-edge? (rest EDGE-LOA)))))
(define EDGE-LOA2-VAL
(and (not (empty? EDGE-LOA2))
(or (alien-at-right-edge? (first EDGE-LOA2))
(any-alien-at-right-edge? (rest EDGE-LOA2)))))
As a final example consider determining if any alien in a given loa has reached
earth. This is another problem that requires oring a list. Such an alien exists in a
given loa if it is not empty and either the first alien has reached earth or one of
the rest of the aliens has reached earth. Once again we specialize the template for a
function on a loa and obtain:
(define EARTH-REACHED-LOA
(list (make-posn 1 11)
(make-posn MAX-IMG-X MAX-IMG-Y)))
;; loa → Boolean
;; Purpose: Determine if any alien has reached earth
(define (any-alien-reached-earth? a-loa)
(and (not (empty? a-loa))
(or (alien-reached-earth? (first a-loa))
(any-alien-reached-earth? (rest a-loa)))))
74 List ORing 293
* Ex. 134 — Design and write a function to determine if a given list of numbers
contains an even number.
* Ex. 135 — Design and write a function to determine if a given list of numbers
contains a multiple of 10.
* Ex. 136 — Design and write a function to determine if a given list of strings
contains "Ekaterina Ermilkina".
** Ex. 137 — A car is defined by a brand, a model, and a price. Design and
write a function to determine if a given list of cars contains a car that costs less
than $20,000.
* Ex. 138 — Design and write a function to determine if a given list of images
contains an image with a width greater than 100 pixels.
75 List ANDing
Another variant of list searching determines if all elements of some list, L, satisfy
some property. That is, we seek to determine if all of L’s elements satisfy a predicate
P. How is this determined? Think about the list varieties. What is the answer if L
is empty? The answer may not be obvious to you. That is fine. Let us once again
consider a list of length 1 whose only element, p, satisfies P. In this case, do all
the list elements satisfy P? Clearly, the answer is yes. Now, think about how this is
computed. The list looks as follows:
L = (cons p '())
Let us name the function that processes L: all-satisfy-P?. To process p you
apply P to p to obtain #true. The rest of the list is processed recursively by calling
all-satisfy-P?. All the list elements, if any, in the rest of L must also satisfy P.
This means that the following expression must evaluate to #true:
(and (P p) (all-satisfy-P? (rest L)))
Given that the rest of L is '(), (all-satisfy-P? (rest L)) must evaluate to
#true in order for (all-satisfy-P? L) to evaluate to #true. Thus, we have that
the answer must be #true when L is empty. When the list is not empty you and
the result of applying P to the first element of the list and the result obtained from
recursively processing the rest of the list.
75 List ANDing 295
Consider determining if all the numbers in a list of numbers are even. If the given
lon is empty then, as suggested above, the answer is true. It is certainly true that all
the numbers (i.e., none) in the empty list are even. If the given lon is not empty, then
the first number must be even and the rest of the numbers must be even. Observe that
no decision must be made to solve this problem. Therefore, the use of a condition
expression as suggested by the template for a function on a lon is not needed. Instead,
the result for the different varieties of lon is ored.
To start specializing the template for a function on a lon as suggested by our
problem analysis let us start by defining useful sample lon instances and sample
expressions for our function all-even?:
;; Sample instances of lon
(define E-LON '())
(define AE-LON '(88 98 22 78 506))
(define NAE-LON '(8 561 683 788))
;; lon2 . . . --> . . .
;; Purpose: . . .
;; (define (f-on-lon2 a-lon2 . . .)
;; (cond [(empty? a-lon2) . . .)
;; [(= (length a-lon2) 1) . . .]
;; [else . . .(f-on-number (first a-lon2))
;; . . .(f-on-number (second a-lon2))
;; . . .(f-on-lon2 (rest a-lon2))
;; . . .(f-on-lon2 (rest (rest a-lon2)))]))
because they yield the same answer. If the given lon2 has two or more elements,
then the first two elements must be in non-decreasing order. How is the recursive
call made? It must not eliminate the second number in the given lon2 because it is
needed to make sure it is in non-decreasing order relative to the third number if it
exists. That is, the recursive call is made with the rest of the given lon2. Observe
that a decision must not be made to compute the needed answer. We can or the result
of determining if the list is of at most length 1 and the result of anding the result
of comparing the first two numbers and the result of recursively processing the rest
of the given lon2. This analysis leads to the following specialization of the function
template:
;; Sample instances of lon2
(define E-LON2 '())
(define A-LON2 '(88))
(define SORTED-LON2 '(8 56 68 788))
(define UNSORTED-LON2 '(5 7 8 4 9))
;; lon2 → Boolean
;; Purpose: To determine if the given lon2 is sorted in
;; nondecreasing order
(define (sorted-lon2? a-lon2)
(or (<= 0 (length a-lon2) 1)
(and (<= (first a-lon2) (second a-lon2))
(sorted-lon2? (rest a-lon2)))))
** Ex. 139 — Design and write a function to determine if a list of posns only
contains aliens as defined for Aliens Attack.
** Ex. 140 — Design and write a function to determine if a list of images only
contains images that have an area less than 200 squared pixels.
*** Ex. 146 — Design and write a function to determine if in a list of numbers,
L, for every even i in [0..(sub (length L))] the i + 1 number, if it exists,
has a different parity than the ith number.
300 13 List Processing
76 List Mapping
List mapping refers to the process of applying a function to every element of a list
and returning a list of the results. This type of operation is very common when
solving problems involving lists. For example, in Aliens Attack multiple aliens and
multiple shots may be represented using, respectively, a list of aliens and a list of
shots. Every time the clock ticks all the aliens and all the shots must be moved. How
is this accomplished?
First consider how to move every alien. Imagine the world in Aliens Attack contains
a list of aliens and a direction. All the aliens must move in the same direction every
time the clock ticks. This means that the function move-alien must be mapped over
the given loa to create the list of the moved aliens. This list of moved aliens becomes
the world’s list of aliens after the clock tick. The list of aliens must be processed,
thus, suggesting specializing the template for a function on a list of aliens. Think
about how to process the list of aliens based on its structure. If the list of aliens
is empty, then there are no aliens to move and the list of moved aliens is empty.
Otherwise, the first alien is moved using the function move-alien and the rest of
the aliens are moved. The list of moved aliens is created by consing these two values.
Based on this problem analysis, the template for a function on a list of aliens may be
specialized starting with the sample expressions as follows:
;; Sample expressions for move-loa
(define MELOA-VAL E-LOA)
(define MILOA-VAL (cons (move-alien (first INIT-LOA) 'left)
(move-loa (rest INIT-LOA) 'left)))
(define MILOA-VAL2 (cons (move-alien (first INIT-LOA) 'right)
(move-loa (rest INIT-LOA) 'right)))
(define MILOA-VAL3 (cons (move-alien (first INIT-LOA) 'down)
(move-loa (rest INIT-LOA) 'down)))
The first sample expression states that when the empty list of aliens is moved the
resulting list of moved aliens is the empty list of aliens. The other three sample
expressions state that a non-empty list of aliens is moved by creating a list containing
the first alien moved in a given direction and the rest of the aliens moved in the same
given direction. There is such a sample expression for each possible instance of dir.
Abstracting over the sample expressions yields the specialization of the definition
template:
;; loa dir → loa
;; Purpose: To move the given loa in the given dir
(define (move-loa a-loa dir)
76 List Mapping 301
Designing a function to move a list of shots is also a mapping operation. The function
move-shot must be mapped over a given list of shots. Given that a list of shots must
be processed, the template for a function on a list of shots must be specialized. The
result of this specialization is:
;; los → los
;; Purpose: To move the given list of shots
(define (move-los a-los)
302 13 List Processing
Both move-loa and move-los return the same type of list that they get as input.
This occurs for move-loa because the mapped function move-alien returns an
alien. Similarly, this occurs for move-los because the mapped function move-shot
returns a shot. This is not a property of mapping a function over a list. Instead, it is a
property of the function mapped. A mapping function may take as input a (listof
X) and return a (listof Y) if the signature of the mapped function is: X → Y.
Consider, for example, computing the lengths of each string in a (listof
string). The length of a string is always a number. More specifically, it is al-
ways a, natnum, natural number (i.e., a nonnegative integer). Therefore, the result
type for a function that solves this problem is: (listof string) → (listof
natnum). The function must process the given (listof string) and compute the
length of each string.
We start the process of specializing the template for a function on a (listof
string) by creating sample instances of (listof string):20
20
Verses courtesy of Lord Byron.
76 List Mapping 303
* Ex. 147 — Design and write a function to cube the numbers in a list of
numbers.
** Ex. 148 — Design and write a function to return a list of x values from a
list of posns.
** Ex. 150 — Display the data definition and the template for a function on a
list of images. Design and write a function to compute the perimeters of a list
of images.
*** Ex. 151 — Design and write a function to double the even numbers and
to triple the odd numbers in a list of numbers.
Search the documentation for functions that transform a number into a string
and vice versa.
77 List Filtering
List filtering refers to the process of removing/filtering elements from a given list
that do not satisfy a condition. Alternatively, you may think of list filtering as
returning/extracting a list of the elements that satisfy a given condition. This is
achieved by testing every element of the list as it is traversed.
Three examples are presented. The first extracts the even numbers out of a given
list of numbers. The second filters out the aliens hit by a shot for a given list of aliens
and a given list of shots. The third extracts shots that have not hit an alien from a
given list of shots and a given list of aliens.
77 List Filtering 305
Consider the problem of returning the even numbers found in a list of numbers. The
list must be traversed. This means that the list must be processed and you ought to
think in terms of the structure of a list of numbers. If the given list is empty, then
it contains no even numbers and the empty list is the list of all even numbers in the
given list. If the given list is not empty, then we think in terms of the first number
and the rest of the numbers. The first number is included in the resulting list if it is
even and it is not included if it is odd. The rest of the list is processed recursively as
suggested by the template for a function on a lon. This analysis suggests that there
are three conditions that need to be determined: the list is empty, the first element of
the list is even, and the first element of the list is not even.
Given our problem analysis, we can proceed to specialize the template for a
function on a lon. First we define sample instances of a lon to use in our tests:
;; Sample instances of lon
(define LON1 '())
(define LON2 '(0 1 2 -3 4 5 -6 7 8 9))
(define LON3 '(11 75 -31 49))
Observe that each list satisfies one of the conditions. LON1 is empty. LON2 has an
even number as its first element. LON3 has an odd number as its first element.
We can now write sample expressions to illustrate how each of the different lists
is processed:
;; Sample expressions for extract-evens
(define LON1-VAL '())
(define LON2-VAL (cons (first LON2)
(extract-evens (rest LON2))))
(define LON3-VAL (extract-evens (rest LON3)))
When the input list is empty, the answer is the empty list. When the first number is
even this number is consed to the result of extracting the even numbers from the
rest of the list. When the first number is not even, then it is not consed to the result
of extracting the even numbers from the rest of the list.
Abstracting over the sample expressions helps us specialize the definition tem-
plate:
;; lon → lon
;; Purpose: To return list of the even numbers in the
;; given list
(define (extract-evens a-lon)
(cond [(empty? a-lon) '()]
[(even? (first a-lon))
(cons (first a-lon) (extract-evens (rest a-lon)))]
[else (extract-evens (rest a-lon))]))
306 13 List Processing
Instead of two stanzas, the conditional has three stanzas as per our problem analysis.
When the given lon is not empty, even? is used to determine if the first number is
even and if so it is added to the resulting list. If it is not even, then number must be
odd and is not added to the resulting list.
The tests using sample computations, as always, are specialized using the sample
lon instances and the defined constants for the values of the sample expressions:
;; Tests using sample computations for extract-evens
(check-expect (extract-evens LON1) LON1-VAL)
(check-expect (extract-evens LON2) LON2-VAL)
(check-expect (extract-evens LON3) LON3-VAL)
The tests using sample computations are specialized using extrema lon instances. In
this case, we use a list that only contains even numbers and a list that only contains
odd numbers:
;; Tests using sample values for extract-evens
(check-expect (extract-evens '(2 4 6 8)) '(2 4 6 8))
(check-expect (extract-evens '(-71 -9 -909 -55)) '())
In Aliens Attack, we wish to have multiple shots and multiple aliens. Among other
things, this means that aliens hit by a shot must be removed from the world’s list of
aliens. This is accomplished by traversing the loa and testing each alien. If the first
alien has been hit by any shot, then it is not added to the resulting loa. Otherwise
the first alien is added to the resulting loa. Consider carefully how to design such
a function. There are two inputs: a list of aliens and a list of shots. We must decide
which template to specialize to write the function. Our problem analysis suggests
that for each alien we must traverse the list of shots to determine if it has been hit by
any of the shots. This means that the function ought to be designed to traverse the
aliens using the template for a function on a loa.
Observe that our problem analysis reveals that there are three conditions and,
therefore, a conditional expression with three stanzas is needed to specialize the
template. This means we need at least three sample expressions to illustrate how
each condition is processed. The following are sample expressions for this purpose:
;; Sample expressions for remove-hit-aliens
(define EMP-LOA-VAL '())
(define INIT-LOA-VAL (cons (first INIT-LOA)
(remove-hit-aliens (rest INIT-LOA)
INIT-LOS)))
(define INIT-LOA-VAL2 (remove-hit-aliens (rest INIT-LOA) LOS2))
These sample expressions use previously defined loa and los instances. The first
expression states that when the given loa is empty the answer is empty. There are
77 List Filtering 307
no aliens that have been hit in the empty loa. The second expression corresponds to
the case when the first alien in the given loa is not hit by any shot. In this case, the
first alien is added to the result list. The third sample expression corresponds to the
case when the first alien is hit. In this case, the first alien is not added to the result
list.
Using a variable instead is concrete values for the loa and the los allows us to
specialize the template definition:
;; loa los → loa
;; Purpose: To remove the aliens from the given loa hit by any
;; shot in the given los
(define (remove-hit-aliens a-loa a-los)
(cond [(empty? a-loa) '()]
[(hit-by-any-shot? (first a-loa) a-los)
(remove-hit-aliens (rest a-loa) a-los)]
[else (cons (first a-loa)
(remove-hit-aliens (rest a-loa) a-los))]))
Observe that there is a stanza for each of the conditions identified. The function
hit-by-any-shot? is an auxiliary predicate to determine if any shot has hit the
first alien. It needs to be designed and written. For now, we assume that the function
exists to fulfill the separation of concerns principle.
The tests using sample computations use the previously defined loa and los
instances and the defined constants for the value of the sample expressions:
;; Tests using sample computations for remove-hit-aliens
(check-expect (remove-hit-aliens E-LOA LOS2) EMP-LOA-VAL)
(check-expect (remove-hit-aliens INIT-LOA INIT-LOS)
INIT-LOA-VAL)
(check-expect (remove-hit-aliens INIT-LOA LOS2) INIT-LOA-VAL2)
The first test using sample values illustrates that the function works when more than
one alien must be removed. Observe that the los includes both shot varieties. The
second test illustrates that the function works when none of the aliens need to be
removed given a non-empty loa and a non-empty los. The tests are the following:
(check-expect (remove-hit-aliens
(cons (make-posn 1 1)
(cons (make-posn 2 2)
(cons (make-posn 3 3)
(cons (make-posn 4 4) '()))))
(cons (make-posn 3 3)
(cons NO-SHOT
(cons (make-posn 1 1) '()))))
(cons (make-posn 2 2)
(cons (make-posn 4 4) '())))
308 13 List Processing
(check-expect
(remove-hit-aliens (cons (make-posn 1 1)
(cons (make-posn 2 2)
(cons (make-posn 3 3) '())))
(cons (make-posn 7 4)
(cons NO-SHOT
(cons (make-posn 3 5)
'()))))
(cons (make-posn 1 1)
(cons (make-posn 2 2)
(cons (make-posn 3 3) '()))))
Now, we must design the auxiliary function hit-by-any-shot?. Given an alien,
how do we determine if the alien has been hit by any shot in a given los? The los
must be traversed to determine if hit?21 holds for any of the shots. Determining
if a predicate holds for any member of a list is a list oring function. Based on our
previous experience in this chapter, a conditional is not required for such a function.
Instead, an and-expression is used to determine that the given list of shots is not
empty and an or-expression is used to determine if the first shot has hit the alien or
some other shot has hit the alien.
Based on our problem analysis, we may write the following sample expressions:
;; Sample expressions for hit-by-any-shot?
(define HIT-LOS1-VAL (and (not (empty? LOS2))
(or (hit? (first LOS2) ALIEN-8-0)
(hit-by-any-shot? ALIEN-8-0
(rest LOS2)))))
(define HIT-LOS2-VAL (and (not (empty? INIT-LOS))
(or (hit? (first INIT-LOS) ALIEN-8-0)
(hit-by-any-shot?
ALIEN-8-0
(rest INIT-LOS)))))
Each sample expression tests if the a los is not empty. If the list of shots is not
empty, then both expressions test if the first shot has hit the alien and test if any other
shot has hit the alien. The first expression uses a non-empty los that contains a shot
that has hit the given alien. The second uses an empty los that, of course, does not
contain any shot that has hit the given alien.
Abstraction over the concrete expressions informs us how to specialize the defi-
nition template for a function on a los as follows:
;; alien los → Boolean
;; Purpose: To determine if the given alien is hit by any shot
;; in the given los
(define (hit-by-any-shot? an-alien a-los)
(and (not (empty? a-los))
(or (hit? (first a-los) an-alien)
(hit-by-any-shot? an-alien (rest a-los)))))
21
The predicate hit? was designed in Sect. 63.1.
77 List Filtering 309
* Ex. 153 — Design and write a function to return the odd numbers in a given
list of numbers.
* Ex. 154 — Design and write a function to extract the strings of length greater
than 5 from a given list of strings.
Design and write a function to extract the images that have an area less than
10,000 pixels2 from a given list of characters.
** Ex. 156 — Design and write a function that extracts the multiples of 3 and
7 from a given list of numbers.
* Ex. 157 — Design and write a function to remove the strings that start with
“M” from a given list of strings.
310 13 List Processing
In Aliens Attack, we need to filter the list of aliens. We must now decide if we must
also filter the list of shots. Should a single shot be able to hit multiple aliens or
should it only be able to hit one alien? We adopt the second posture. This means
that the list of shots must be filtered to eliminate shots that have hit an alien. As we
now know, filtering requires traversing a given list to create a new list with elements
that satisfy a property. In this case, we need to extract the shots that have not hit an
alien. Think carefully about what it means to have a shot that has not hit an alien.
Which shots are these? Clearly, a posn shot that does not have the same image-x
and image-y coordinates as any alien is a shot that must be preserved. What shots
need to be filtered out? A posn shot that has the same coordinates as any alien
must be filtered out. In addition, all NO-SHOT will also be filtered out because such
a shot no longer can affect the evolution of the game. To filter the shots we need as
input the list of shots and the list of aliens. As the list of shots is traversed any shot
that is NO-SHOT or a posn shot that has hit an alien is not added to the resulting
list of shots. Otherwise, the shot is added to the resulting list of shots.
The sample expressions in the template for a function on a list of shots may be
specialized as follows:
;; Sample loa instances
(define LOA3 (list (make-posn 1 9) (make-posn 8 0)))
(define LOA4 (list (make-posn 1 9) (make-posn 8 5)))
Observe that a shot is filtered out if it is either NO-SHOT or it has hit an alien. That is,
the shot is not consed into the resulting list of shots. Determining if the first shot has
hit an alien requires the given loa to be traversed. This means processing a different
kind of data and, therefore, this task is relegated to an auxiliary function that needs
as input a shot and an loa. Once again, we see the importance of separation of
concerns in program design.
The tests are specialized as follows:
;; Tests using sample computations for remove-shots
(check-expect (remove-shots INIT-LOS INIT-LOA)
RM-INIT-LOS-VAL)
(check-expect (remove-shots LOS2 (list (make-posn 1 9)
(make-posn 8 0)))
RM-LOS2-VAL)
(check-expect (remove-shots LOS2 (list (make-posn 1 9)
(make-posn 8 5)))
RM-LOS2-VAL2)
(define HIT-ANY-ALIEN3
(and (not (empty? INIT-LOA))
(or (hit? SHOT5 (first INIT-LOA))
(hit-any-alien? SHOT5 (rest INIT-LOA)))))
A new shot instance is defined to facilitate the writing of the sample expressions.
There are four sample expressions in order to have expressions for both varieties
of aliens and shots. There are four possible combinations as input. The computa-
tion for an empty loa and a NO-SHOT is illustrated by the first sample expression.
The computation for an empty loa and a posn shot is illustrated by the second
sample expression. The computation for a non-empty loa and a NO-SHOT is il-
lustrated by the third sample expression. The computation for a non-empty loa
and a posn shot is illustrated by the fourth sample expression. The previously
defined hit? (in Chap. 11) is used to detect if a shot has hit an alien. This is
consistent with the goal to recycle as much code as possible during a refinement
step.
Abstracting over the sample expressions leads to the following function definition:
;; shot loa → Boolean
;; Purpose: Determine if the given shot has hit any alien
;; in the given loa
(define (hit-any-alien? a-shot a-loa)
(and (not (empty? a-loa))
(or (hit? a-shot (first a-loa))
(hit-any-alien? a-shot (rest a-loa)))))
Observe that this is, indeed, very similar to other list ORing functions developed in
this chapter.
The function is tested as follows:
;; Tests using sample computations for hit-any-alien?
(check-expect (hit-any-alien? NO-SHOT E-LOA)
HIT-ANY-ALIEN0)
(check-expect (hit-any-alien? SHOT3 E-LOA)
HIT-ANY-ALIEN1)
77 List Filtering 313
* Ex. 158 — Design and write a function to extract the odd numbers from a
given list of numbers.
** Ex. 159 — Design and write a function to filter out numbers that are not a
multiple of 9 from a given list of numbers.
** Ex. 160 — Design and write a function to filter out the NO-SHOTs from a
given list of shots.
*** Ex. 161 — Design and write a function to filter out from a list of numbers
any number that appears in another given list of numbers.
Design and write a function to extract from a list of items those that are below
a given price.
78 List Sorting
The solution to many problems requires sorting a list. In order for a list to be sortable,
it must contain numerical or ordinal data. That is, any two elements are comparable
to determine which goes first and which goes second. For example, a list of numbers
and a list of strings may be sorted. A list that contains nominal data is not sortable.
For example, a list of symbols or a list of religions is not sortable.22 For sortable data,
we need a predicate that determines which of two elements goes first. For example,
≤ and ≥ are two such predicates for numbers depending on whether the list must be
sorted in non-decreasing order or in non-increasing order.
Let us take as an example sorting a list of numbers in non-decreasing order. For
example, '(87 65 90 21) after sorted in non-decreasing order is '(21 65 87
90). How is this accomplished? Clearly, the given list must be traversed and a new
list must be constructed. Therefore, think about the varieties of a list of numbers.
If the given lon is empty, then the sorted lon is also empty. If the given list is not
empty, think about its structure. The template for a function on an lon tells us to
process the rest of the list recursively. Assuming the function is named sort-lon
and the given list is a-lon, think about what this expression evaluates to:
(sort-lon (rest a-lon))
This expression ought to evaluate to a sorted list containing all the elements of
a-lon except a-lon’s first element. This means that we must combine a-lon’s first
element with a sorted list. How is this done? The sorted list must be traversed to
find the right place to insert a-lon’s first element. If this is done, then we have a
sorted list that contains all the elements of the given list. Take note that inserting
a number into a sorted list of numbers is a different problem from sorting a list of
numbers. A different problem means we need an auxiliary function to perform this
computation. Keeping consistent with the principle of separation of concerns, we
assume this inserting function exists and works.
22
The words (i.e., strings) that represent the names of different religions are sortable, but this is a
property of strings and not a property of religions.
78 List Sorting 315
Based on our problem analysis, template specialization begins with a-lon in-
stances and sample expressions as follows:
;; Sample instances of a lon
(define E-LON '())
(define SORTED-LON '(17 18 29 37 41 52))
(define UNSORTED-LON '(89 21 1 77 23))
* Ex. 163 — Design and write a function that sorts a list of numbers in non-
increasing order.
Design and write a function that sorts a list of items in non-decreasing order by
price.
318 13 List Processing
*** Ex. 165 — Stores usually need space for new inventory and put on sale
the items they have the most of in stock. Using the item data definition from
the previous problem, design and write a function that sorts a list of items in
non-increasing order by quantity.
In this chapter we explore a different data type of arbitrary size. This is a data type
that is a subtype of the number type. It is likely that you have seen this data type in
your Mathematics textbooks. In a Mathematics textbook you may see the following
definition for the natural numbers:
The natural numbers are 0, 1, 2, . . .
You probably interpret the . . . as meaning all the way to infinity. Thus, you are
confident that you understand what the natural numbers are. A natural question that
you may be asked is: What is a natural number? You may answer that a natural
number is an element of the set of the natural numbers. This may be true, but is
it useful in problem solving? That is, can you solve a problem if the given data is
a natural number? How do you process a natural number? You may say that you
process a natural number the same way you process a number. Yes, of course, this is
possible for some problems because the natural numbers are a subset of the numbers.
Is it always the case? If n is a natural number how you compute n!? In a Mathematics
textbook you may find the following:
n! = 1 * 2 * * 3 * . . . * n-1 * n
Do you understand how to compute n!? You can probable compute 3! and 5!.
Can you compute 1000!? If the answer is no then you need to write a function
to perform the computation. Can you do this? There are several problems with the
approach taken in some Mathematics textbooks for our purposes. A value needs to
be computed from a natural number, but we do not have a data definition for a natural
number. In addition, we do not know how to program . . . .
Consider more carefully developing a data definition for the set of natural numbers.
A natural number may be small like 0. It may be large like 10000. It may be of medium
size like 1587. It may be super large like 100000000. What is all this telling us?
It is telling us that a natural number is data of arbitrary size. What do we need to
design functions that process a natural number? You have probably already realized
the answer. We need a recursive data definition and a function template.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 319
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_14
320 14 Natural Numbers
Recall that for a recursive data definition we need at least two varieties. At least
one variety must not have a selfreference and at least one variety must have a
selfreference. Let us start with the variety that does not have a selfreference. When
we studied lists, the nonrecursive variety was the smallest list possible: '(). What
is the smallest natural number? Clearly, it is 0. Therefore, for our data definition we
can say that a natural number is 0. Now, ask yourself how can other natural numbers
be created? To make a larger list we use cons. What can we use to make a bigger
natural number? Let us start with 0, which we know is a natural number. Can we
create the next natural number, 1, using 0? Sure, we increment 0 by 1. Can we build,
2, the next natural number? Well we can increment the natural number 1 by 1. We
can build 3 by incrementing 2 by 1. There is a pattern here. If n is a natural number,
the next natural is obtained by incrementing it by 1. We can now write the needed
data definition:
A natural number (natnum) is either:
1. 0
2. (add1 natnum)
Observe that there are two natnum varieties of which one does not have a selfrefer-
ence and other does.
We can use add1 to create a new natnum. What is the selector function to extract
the natnum used to build a given nonzero natnum? What natnum is used to build 5?
It is 4. What natnum is used to build 10? It is 9. Again we see a pattern. The selector
function is sub1.
Observe that there is one selfreference in the data definition. This means that
processing a nonzero natnum requires one recursive call. Based on this observation
the template for a function on a natnum is the following:
#| ;; Sample instances of natnum
(define ZERO 0)
(define NATNUMA . . .)
...
;; natnum . . . → . . .
;; Purpose: . . .
(define (f-on-natnum a-natnum . . .)
(if (= a-natnum 0)
...
(. . .a-natnum. . .(f-on-natnum (sub1 a-natnum) . . .))))
81 Computing Factorial
Armed with the data definition and the template for a function on a natnum we are
ready to tackle problem solving. We start with designing a function to compute the
factorial of a natural number. First we define a few sample natnums as follows:
;; Sample instances of natnum
(define ZERO 0)
(define TEN 10)
(define FIFTY 50)
Two nonzero natnums are defined to facilitate the abstraction process over the sample
expressions. The values chosen are ones for which anyone is unlikely to compute its
factorial by hand. Thus, a function is really needed.
Next, we must analyze how to solve the problem. For this we reason statically
about the structure of a natnum (just as we reason statically about the structure of a
list). Start with the nonrecursive variety. What is the factorial of 0? The mathematical
definition for factorial above does not specify this. It is not clear what the value of
0! is. Let us proceed with the second variety and come back to the first variety later.
If the given natnum, n, is not 0, the template suggests combining the given number
and the factorial of n - 1. For example, for the factorial of 4 the template suggests
combining 4 and 3!. We know from the mathematical definition above that:
3! = 1 * 2 * 3
Given that value of 3! and the value 4, how do we get the value of 4!? Let us use
the mathematical definition again to examine what 4! is
4! = 1 * 2 * 3 * 4
322 14 Natural Numbers
It now becomes clear that 4 and the value of 3! must be multiplied. This informs us
that when the given natnum, n, is not 0, then n and (n - 1)! must be multiplied
to obtain n!. We can now turn back to think about what the value of 0! must be. A
value must be returned when n is 0. Let us look at how 4! is computed:
4! = 4 * 3 * 2 * 1 * ??
This sample computation captures the design idea of multiplying 4 by 3!. At each
step n is decremented by 1. Eventually, n becomes 0. The ?? represents the value of
0!. Observe that the product of 4 through 1 is 4!. It now becomes clear that 0! is 1.
Any other value for 0! would make the product not equal to 4!. Finally, observe that
the product of natural numbers is a natural number. For example, 4! is 24, which
is a natnum. This means that any function that computes the factorial of a natnum
must return a natnum.
With a clear idea of how to compute the factorial of a natnum, we write sample
expressions:
;; Sample expressions for factorial
(define ZERO-VAL 1)
(define TEN-VAL (* 10 (factorial (sub1 10))))
(define FIFTY-VAL (* 50 (factorial (sub1 50))))
The first sample expression illustrates the computation when the given natnum is 0.
The other two sample expressions illustrate the computation when the given natnum
is not 0. As per our problem analysis the given natnum is multiplied by the factorial
of said number decremented by 1.
Abstraction over the sample expressions yields the specialization of the definition
template
;; natnum → natnum
;; Purpose: Compute the factorial of the given natnum
(define (factorial a-natnum)
(if (= a-natnum 0)
1
(* a-natnum (factorial (sub1 a-natnum)))))
Observe that the return type is natnum as suggested by our problem analysis. In the
body of the function 1 is returned if the given natnum is 0. Otherwise, the product
of the given natnum, a-natnum, and the factorial of a-natnum - 1 is returned. In
other words, the values returned are consistent with our problem analysis.
The final template specialization step is to develop the tests. The tests using
sample computations are
;; Tests using sample computations for factorial
(check-expect (factorial ZERO) ZERO-VAL)
(check-expect (factorial TEN) TEN-VAL)
(check-expect (factorial FIFTY) FIFTY-VAL)
82 Computing Tetrahedral Numbers 323
These tests, as before, illustrate that the function correctly computes the values of
the sample expressions. They validate the abstraction over the sample expressions.
The tests using sample values are
;; Tests using sample values for f-on-natnum
(check-expect (factorial 3) 6)
(check-expect (factorial 5) 120)
The sample values chosen are small enough that any reader of the code can easily
verify that the expected values are correct. To finalize the steps of the design recipe
run the program to make sure all the tests pass.
* Ex. 166 — Design and implement a function to compute the sum of the first
n squares, where n is a natural number. For example, for n = 3 the sum of the
first 3 nonzero squares is: 32 + 22 + 12 .
** Ex. 168 — Design and implement a function to compute the product of two
natural numbers only using addition. For example, 3 * 4 = 3 + 3 + 3 + 3.
Consider the structure of a natural number. How many disks are needed to build
a tetrahedral of height 0? If the height is 0, it means that it contains no disks.
Therefore, the 0th tetrahedral number is 0. How many disks are needed if the height
is not 0? Once again, consider the tetrahedral in Fig. 70c. We can use a divide and
conquer strategy to determine how to compute its tetrahedral number. Divide the
tetrahedral into two parts: the bottommost layer and all the other layers. This division
is displayed in Fig. 71. Figure 71a is a tetrahedral of height 2. The template suggests
that this value be computed recursively. How do we compute the number of disks in
the bottom layer? Recall that each layer is a triangle. Observe that the bottommost
layer is a triangle of height 3 (i.e., three layers). This means that we need to compute
the number of coins needed for a triangle of height 3. The number of disks needed
to build a triangle is a triangular number. In general, when the height, h, is not zero,
we need to compute hth triangular number and the (h - 1)th tetrahedral. How
are these two numbers combined to obtain the hth tetrahedral number? Once again
looking at Fig. 71, it becomes clear that these two numbers must be added.
Based on the problem analysis we define natnum instances and sample expres-
sions as follows:
;; Sample instances of natnum
(define ZERO 0)
(define NINE 9)
(define SIXTY 60)
is simply 0 as per our problem analysis. The second and third sample expressions
illustrate that for a given nonzero natnum you add the triangular number for the given
natnum and recursively compute the tetrahedral number for the input’s previous
natnum.
To specialize the template abstraction over the sample expressions is performed
to obtain:
;; natnum → natnum
;; Purpose: Compute the nth tetrahedral number
(define (nth-tetra a-natnum)
(if (= a-natnum 0)
0
(+ (nth-tri a-natnum) (nth-tetra (sub1 a-natnum)))))
Once again, our design assumes that the auxiliary function exists and works correctly.
The tests are the following:
;; Tests using sample computations for nth-tetra
(check-expect (nth-tetra ZERO) ZERO-VAL)
(check-expect (nth-tetra NINE) NINE-VAL)
level and recursively compute the disks needed for a triangle of height 1. Observe
that 2 + 1 = 3, the number of disks needed for a triangle of height 2. For height 3
the number of disks is 3 + the number of disks needed for height 2 = 6. The pattern
holds. The given height is added to the triangular number obtained from height −1.
For example, for height 3:
triangular number for 3
= 3 + 2 + 1
= 3 + 3
= 3 + triangular number for 2
= height + triangular number for (height - 1)
Based on our problem analysis, we can write sample expressions for nth-tri:
;; Sample expressions for nth-tri
(define ZERO-VAL-TRI 0)
(define NINE-VAL-TRI (+ NINE (nth-tri (sub1 NINE))))
(define SIXTY-VAL-TRI (+ SIXTY (nth-tri (sub1 SIXTY))))
The same natnum instances defined for nth-tetra are used. The first expression
illustrates that for 0 the answer is simply 0 as per our problem analysis. The second
and third sample expressions illustrate that for a nonzero natnum the given natnum
is added to the triangular number obtained from the decremented given natnum.
Abstraction over the sample expressions is used to specialize the function defini-
tion template:
;; natnum → natnum
;; Purpose: Compute the triangular number for the given natnum
(define (nth-tri a-natnum)
(if (= a-natnum 0)
0
(+ a-natnum (nth-tri (sub1 a-natnum)))))
Observe that the return value is a natnum because 0 is a natnum and adding two
natnums results in a natnum. The body of the function implements the design
obtained from our problem analysis.
The tests may be written as follows:
;; Tests using sample computations for nth-tri
(check-expect (nth-tri ZERO) ZERO-VAL-TRI)
(check-expect (nth-tri NINE) NINE-VAL-TRI)
(check-expect (nth-tri SIXTY) SIXTY-VAL-TRI)
* Ex. 169 — The number of disks used for a triangle is equal to the sum of
the natural numbers from the given height, h, down to 0. This sum is equal to
h(h+1)
2 . Refactor nth-tri to take advantage of this formula.
83 Making Copies
* Ex. 170 — Design and implement a function that returns the first n squares,
where n is a natural number.
** Ex. 171 — Let f be
f(x) = 5x5 - 10x3 + 5x + 87
Design and implement a function that takes a natural number, n, as input and
that returns a list of the values of f(x), for natural number x in [n..0].
**** Ex. 172 — The natural numbers greater than 100, natnum>100, are a
subtype of the natural numbers. Provide a data definition for natnum>100.
Design and write a function that takes as input a natnum>100, k, and that
returns the sum of the natural numbers greater than 100 and less than or equal
to k.
Chapter 5 introduced interval types to describe a value and used to design functions
that make decisions. This chapter returns to intervals, but not as a mechanism solely
used to describe a value. Instead, intervals are considered compound data that may
be processed. Chapter 14 hinted at the concept of an interval as data that may be
processed. If you think about it carefully, processing a natnum, n, means that all the
natural numbers in [0..n] must be processed. The first call to the function does
something for n, the second call does something for n - 1, and so forth until the last
call does something for 0. Processing natural numbers fixes the lower end of the
interval to 0, while the higher end of the interval is of arbitrary size.
This chapter generalizes the concept of an interval such that both ends may
vary. For example, in your Mathematics textbooks you may have seen an interval of
integers described as
[i..j], where i < j
This is intended to represent all the integers between i and j. The square brackets
indicate that i and j are included in the interval. In other words, [i..j] represents
all the integers greater than or equal to i and less than or equal to j. If a parenthesis
is used, it means that the value is not included. For example, the following means
that i is not included in the interval:
(i..j], where i < j
From this you can surmise the intended meaning of [i..j) and (i..j).
The question now is how do you process an interval of integers. The description
found in many Mathematics textbooks hides the structure of an interval making it hard
to reason about how an interval is processed. Clearly, an interval is data of arbitrary
size and, therefore, we need a recursive data definition. Can you discern from the
mathematical description above what the base case or cases are? For example, can
an interval be empty?
Understanding to how to process intervals is important. In future courses you will
study compound data called a vector (or array). Usually, the part of the vector
that needs to be processed is defined by an interval of natural numbers (a subtype
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 331
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_15
332 15 Interval Processing
of the integers). Before studying vector intervals, however, this chapter focuses on
integer intervals.
Without loss of generality, we shall focus on intervals that include their end points,
that is, on intervals such as [i..j]. It is straightforward for you to extend the work
presented to an interval that does not include one or both end points.
Consider what the interval [-2..4] represents
[-2 -1 0 1 2 3 4]
It represents 7 consecutive integers. This fact, however, tells us nothing about the
structure of an interval that we need to determine in order to process an interval.
A natural question to ask what is the smallest interval possible. For example, is
[23..23] the smallest an interval can be? It only contains 1 number: 23. Can an
interval contain 0 numbers? Thinking carefully about this ought to lead you to the
conclusion that an interval can contain 0 numbers. In other words, an interval may
be empty—a fact well-hidden in the mathematical description above. For example,
[0..-1] is an empty interval. There are no integers greater than or equal to 0 and
less than or equal to -1. Given an interval [low..high], how do we know that the
interval is empty? If low is greater than high, then the interval is empty.
How do we define a non-empty interval? Once again consider [-2..4]:
[-2 -1 0 1 2 3 4]
Here low = -2 and high = 4. Is there any natural way to decompose this interval
into two or more parts? Given that the interval is not empty it must have at least one
number. Let us take that number to be, 4, the high-end of the interval. We have left:
[-2 -1 0 1 2 3]
This is an interval. Specifically, it is [-2..3]. We can rewrite [-2..4] as follows:
[[-2..3] 4]
This means that a non-empty interval has a structure: the high number and the
subinterval with the rest of the numbers. Observe that the high number of the
subinterval is 3. That is, the high end of the given interval is decremented by 1 (i.e.,
(sub1 4) = 3). We can now write a data definition for an interval as follows:
An interval ([low..high]) is two integers such that it is
either the:
1. empty interval (interpretation: low > high)
2. non-empty interval (interpretation: low ≤ high)
[[low..(sub1 high)] high]
85 Interval Data Definition 333
;; [int..int] . . . ...
;; Purpose: . . .
(define (f-on-interval low high . . .)
(if (> low high)
...
(. . .high. . .(f-on-interval low (sub1 high). . .
. . .low. . ..(f-on-interval (add1 low) high). . .)))
Observe that an interval is compound data. It is always represented using two integers.
At first, this might suggest it is data of finite size. However, remember that an interval
represents an arbitrary number of consecutive integers. Further observe that the data
definition contains information about how to interpret each variety of interval. This
is important because it explains how the two varieties are distinguished. For an
empty interval low is greater than high. For a non-empty interval, we have high
and the subinterval [low..(sub1 high)]. This definition suggests that an interval
is processed from high down to low until the lower subinterval is empty.
The choice of dividing an interval into high and [low..(sub1 high)] seems
rather arbitrary. Just as easily we can define a non-empty interval as
[low [(add1 low)..high]
The subinterval may be on the high end. This definition suggests that an interval
is processed from low to high until the higher subinterval is empty. Which is
correct? They are both correct. Observe that in both cases the subinterval is smaller,
which tells us that the circularity in the data definition is not infinite. Eventually, the
subinterval is empty and the circularity stops. This means we can refine the interval
data definition to be:
An interval ([low..high]) is two integers such that it
is either:
334 15 Interval Processing
86 Revisiting Factorial
;; natnum natnum
;; Purpose: Compute the factorial of the given natnum
(define (factorial a-natnum) (interval-product 1 a-natnum))
to low. Now, think about the structure of an interval. If the given interval is empty,
a number that does not change a product must be returned. The only such value is
1. Recall that any number, x, times 1 is x. If the interval is not empty, the function
template suggests creating the final product using high and the product obtained
from [low..high-1]. All that is needed is to multiply these two values.
Based on this problem analysis, we may write interval instances and sample
expressions as follows:
;; Sample instances of interval
(define LOW1 1)
(define HIGH1 0)
(define LOW2 -3)
(define HIGH2 5)
(define LOW3 4)
(define HIGH3 7)
our problem analysis, multiply the high-end value with the product of the rest of the
interval.
Abstracting over the sample expressions yields the following specialization of the
definition template:
;; [int..int] → int
;; Purpose: Compute the product of the integers in the
;; given interval
(define (interval-product low high)
(if (> low high)
1
(* high (interval-product low (sub1 high)))))
Observe that the function implements our design idea by distinguishing between an
empty and a non-empty interval.
Finally, tests may be written as follows:
;; Tests using sample computations for interval-product
(check-expect (interval-product LOW1 HIGH1) LOW1-HIGH1-VAL)
(check-expect (interval-product LOW2 HIGH2) LOW2-HIGH2-VAL)
(check-expect (interval-product LOW3 HIGH3) LOW3-HIGH3-VAL)
For the next Aliens Attack version an initial army of aliens must be created. This
army of aliens is represented as a list of aliens. Consider the problem of creating this
list. Figure 75 displays Aliens Attack with an initial list of aliens. The list of aliens
87 Creating an Army of Aliens 337
contains aliens in three different lines. Each line has the same number of aliens lined
up to form columns. Therefore, a decision must be made on the number of alien
lines and the number of aliens per line. For the army of aliens displayed in Fig. 75
the following constants are defined:
(define ALIEN-LINES 3)
(define ALIENS-PER-LINE 12)
The aliens start at the top row. That is, each alien in the top row of the scene has an
image-y of 0. The alien line is placed toward the scene’s horizontal middle and the
aliens span the same range of image-x values in every line.
These observations give us insight into how to create the initial list of aliens. The
number of alien lines that need to be computed, num-lines, is a natural number.
We can process this natural number to create the needed list of aliens. At each step,
a new alien line is added to the list of aliens using the number of lines that still need
to be computed to determine the image-y value for the aliens in the new line. Each
line of aliens may be of arbitrary size and, therefore, is represented as a list of aliens.
Now, think about the structure of a natural number. If num-lines is 0, then no
more alien lines need to be added and the empty list of aliens is returned. Otherwise,
a line of aliens is created for the target range of image-x values and this new
line is added to the list of aliens obtained by recursively processing num-lines’s
predecessor. Observe that the range of image-x values are consecutive values. That
is, it may be represented using an interval.
This design idea is still incomplete, but it allows us to start specializing the sample
instances and the sample expressions required by the template for a function on a
natural number as follows:
338 15 Interval Processing
Fig. 76 Number of alien lines left and image-y values for alien line
num-lines image-y Value
num-lines image-y Value 5 4
3 2 4 3
2 1 3 2
1 0 2 1
1 0
To specialize the definition template we use abstraction over the sample expressions
to obtain
;; natnum → loa
;; Purpose: Create initial alien army with the given number
;; of lines
(define (create-alien-army num-lines)
(if (= num-lines 0)
'()
(append (make-alien-line (sub1 num-lines)
ALIEN-LINE-XLOW
ALIEN-LINE-XHIGH)
(create-alien-army (sub1 num-lines)))))
The tests are written as follows:
;; Tests using sample computations for create-alien-army
(check-expect (create-alien-army ZERO) ZERO-VAL)
(check-expect (create-alien-army THREE) THREE-VAL)
(check-expect (create-alien-army FIVE) FIVE-VAL)
You may recall from high school Mathematics that a prime number is a natural
number greater than 1 that is not the product of smaller natural numbers. For example,
7 is a prime number because it is not divisible by any natural number in [2..6].
On the other hand, 51 is not a prime number because it is divisible by 3 and 17. To
determine if a given natural number, n, greater than 1 is prime, we must establish
that it is not divisible by any natural number in [2..n]. It is not necessary to
check all the natural numbers in this interval. Observe that it suffices to check if any
natural in [2..(quotient n 2)] divides n because none of the natural numbers
in [(quotient n 2)..n-1] divide n.
Consider the problem of finding the largest prime number in a given interval. As
you know, you have two options: you can process the interval from low to high or
from high down to low. Does it matter which one you choose? Consider finding
the largest prime in [9..22]. If this interval is processed from low to high, you
discover that 9 and 10 are not prime. This means that may think of the interval as
divided into two pieces:
[9..10]: interval has no primes
[11..22]: interval not explored and may have a largest prime
In the next step you determine that 11 is prime. Now, you may think of the interval
in three pieces:
[9..10]: interval has no primes
[11..11]: 11 is the smallest prime
[12..22]: interval not explored and may have a larger prime
How do you determine if 11 is the largest prime? You need to find, if it exists, the
largest prime in [12..22]. If it exists, then that number is the largest prime. If it
does not exist, then 11 is the largest prime. In this example, it is clear that 11 is not
the largest prime but you do not know that until the entire unexplored interval is
processed. This uncertainty requires the function to somehow remember 11 as the
previous largest prime in case the unexplored interval does not have a prime.
Now consider processing the interval from high down to low. This means that
you first discover that 22, 21, and 20 are not primes. You may think of the interval
divided as follows:
[20..22]: interval has no primes
[9..19]: interval not explored and may have a largest prime
In the next step you determine that 19 is a prime and you may think of the interval
divided as follows:
[20..22]: interval has no primes
[19..19]: 19 is the largest prime
[9..18]: interval not explored and may have a largest prime
88 Largest Prime in an Interval 343
You immediately know that the first prime found is the largest one. There is no need
to process the unexplored interval. Clearly, for this problem it is easier to process the
interval from high down to low. If the given interval is empty, the function returns
-1 to indicate that the interval does not contain a prime number. If the interval is not
empty, the largest number, high, in the interval is tested to see if it is not divisible by
any natural number in [2..(quotient high 2]. If so, high is returned as it is the
largest prime. Otherwise, the rest of the interval is recursively processed. Observe
that there are three, not two, conditions that need to be distinguished.
Based on the problem analysis, the following three interval instances and three
sample expressions are developed:
;; Sample instances of interval
(define LOW1 2)
(define HIGH1 1)
(define LOW2 2)
(define HIGH2 23)
(define LOW3 13)
(define HIGH3 16)
natural number in [2..(quotient high 2)]. If it is not, high is the largest prime
as outlined in our problem analysis. Otherwise, the rest of the interval is recursively
searched for a prime.
We may specialize the tests as follows:
;; Tests using sample computations for largest-prime
(check-expect (largest-prime LOW1 HIGH1) LOW1-HIGH1-VAL)
(check-expect (largest-prime LOW2 HIGH2) LOW2-HIGH2-VAL)
(check-expect (largest-prime LOW3 HIGH3) LOW3-HIGH3-VAL)
to be processed requires a different function to be designed. Such is the case for this
example. Furthermore, the different intervals do not have to be processed in the same
direction.
* Ex. 173 — Design and implement a function to sum the integers in a given
interval.
** Ex. 174 — Design and implement a function to sum the even positive inte-
gers in a given interval.
*** Ex. 176 — BSL+ includes the function list-ref that has the following
signature:
(listof X) natnum → X throws error
This function returns the ith element of the list (counting from 0). If the list is
too short, then it does not have an ith element and the function throws an error.
Design and implement a function to determine if a given (listof String)
contains a given string in a given interval. For example, the following tests
should pass:
(check-expect
(contains-in? "Basia"
(list "Walter" "Skyler" "Basia" "Madrid")
0
2)
#true)
(check-expect
(contains-in? "Bob"
(list "Walter" "Skyler" "Basia" "Madrid")
0
1)
#false)
This chapter presents the next refinement for Aliens Attack that adds multiple aliens
and shots. In the past few chapters some of the needed auxiliary functions have been
designed. This chapter brings together the design of the refinement with the already
designed auxiliary functions. The first step is to refine the data definition for a world
and, consequently, the template for functions on a world. This will lead us to the
refinements needed to update the game.
This refinement is the largest one so far. This is why auxiliary functions were
defined throughout the previous chapters (Chaps. 13–15). In this manner, the refine-
ment is not overwhelming. Such an approach is common in software engineering.
With a data representation idea in place, auxiliary functions may be designed first.
In other words, a bottom-up approach may be used. A primary lesson is that in the
software development process both top-down and bottom-up designs may coexist.
The world refinement changes the game from having a single alien and a single shot
to having multiple aliens and multiple shots. The data definitions for a list of aliens
(loa) and a list of shots (los) were defined in Sect. 67. We integrate these to the
world data definition as follows:
;; A world is a structure: (make-world rocket loa dir los)
(define-struct world (rocket aliens dir shots))
Observe that the names of the selectors change from world-alien and world-shot
to, respectively, world-aliens and world-shots. We do so to make sure the
structure’s field names convey to any reader of our code what they represent. This
refinement also means that the template for a function on a world must also be
refined. Specifically, a function on a world should no longer call a function on an
alien or a function on a shot. Instead, it must call functions on a loa and on a los.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 349
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_16
350 16 Aliens Attack Version 4
Instead of drawing an alien and a shot, drawing the world now means drawing a
loa and a los. The sample expressions for draw-world must be refined. To do so
we must realize that two new problems must be solved: drawing a list of aliens and
drawing a list of shots. We assume these functions exist and that they need as input
the proper list to draw and the scene to draw in. The refined sample expressions are
;; Sample expressions for draw-world
(define WORLD-SCN1
(draw-los (world-shots INIT-WORLD)
(draw-loa (world-aliens INIT-WORLD)
(draw-rocket
(world-rocket INIT-WORLD)
E-SCENE))))
(define WORLD-SCN2
(draw-los (world-shots INIT-WORLD2)
(draw-loa (world-aliens INIT-WORLD2)
(draw-rocket
(world-rocket INIT-WORLD2)
E-SCENE))))
Abstracting over the sample expressions yields the refined world drawing function:
;; world → scene
;; Purpose: To draw the world in E-SCENE
(define (draw-world a-world)
(draw-los (world-shots a-world)
(draw-loa (world-aliens a-world)
(draw-rocket (world-rocket a-world)
E-SCENE))))
352 16 Aliens Attack Version 4
It is important not to forget to refine the tests. The tests using sample computations
do not need to be refined because they do not explicitly refer to a world. The tests
using sample values, on the other hand, do explicitly refer to world instances. Each
test is refined as follows to use a loa and a los:
;; Tests using sample computations for draw-world
(check-expect (draw-world (make-world INIT-ROCKET2
(list INIT-ALIEN)
DIR3
empty))
)
Next, it is necessary to design the two auxiliary functions. To draw the aliens a
list of aliens must be processed. This suggests reasoning about the structure of a
loa. If the given list of aliens is empty, there are no aliens to draw and the result is
the given scene. If the given list of aliens is not empty, then the rest of the aliens are
recursively processed to obtain a scene that contains all the aliens except the first. In
this scene the first alien is drawn to obtain a scene that contains all the aliens.
Based on this design idea we can write the following sample expressions:
;; Sample expressions for draw-loa
(define ELOA-VAL E-SCENE)
(define ILOA-VAL (draw-alien (first INIT-LOA)
(draw-loa (rest INIT-LOA)
E-SCENE)))
91 The draw-world Refinement 353
There is a sample expression for each loa variety. The sample expression for a
non-empty loa uses the previously designed draw-alien function from Sect. 49.2.
Based on the sample expressions the definition template is specialized as follows:
;; loa scene → scene
;; Purpose: To draw the given loa in the given scene
(define (draw-loa a-loa scn)
(if (empty? a-loa)
scn
(draw-alien (first a-loa)
(draw-loa (rest a-loa) scn))))
The test templates are specialized as follows:
;; Tests using sample computations for draw-loa
(check-expect (draw-loa E-LOA E-SCENE) ELOA-VAL)
(check-expect (draw-loa INIT-LOA E-SCENE) ILOA-VAL)
)
Given that there are no new problems to solve for drawing a list of aliens the design
of draw-aliens is complete.
To draw the shots a list of shots must be processed. This suggests reasoning about
the structure of a los. If the given list of shots is empty, there are no shots to draw
and the result is the given scene. If the given los is not empty, then the rest of the
shots are recursively processed to obtain a scene that contains all the shots except
the first. In this scene the first shot is drawn to obtain a scene that contains all the
shots.
This problem analysis leads to the following sample expressions:
;; Sample expressions for draw-los
(define ELOS-VAL E-SCENE)
(define ALOS-VAL (draw-shot (first A-LOS)
(draw-los (rest A-LOS) E-SCENE)))
354 16 Aliens Attack Version 4
Observe that the draw-shot function, developed in Sect. 60, is used when the given
los is not empty. As with the sample expressions for draw-aliens, abstraction over
the sample expressions for draw-los leads to the definition template specialization:
;; los scene → scene
;; Purpose: To draw the given los in the given scene
(define (draw-los a-los scn)
(if (empty? a-los)
scn
(draw-shot (first a-los)
(draw-los (rest a-los) scn))))
Observe that draw-los is remarkably similar to draw-loa. This is clearly suggesting
that an abstraction ought to be possible to avoid all the repetition. We shall explore
this soon in the next part of this book.
The tests may be written as follows:
;; Tests using sample computations for draw-los
(check-expect (draw-los E-LOS E-SCENE) ELOS-VAL)
(check-expect (draw-los A-LOS E-SCENE) ALOS-VAL)
;; Tests using sample values for draw-los
(check-expect (draw-los (cons (make-posn 14 8)
(cons (make-posn 3 2) empty))
E-SCENE2)
)
The tests complete the design of draw-los. Given that there are no more problems
to solve the refinement of draw-world is completed.
(check-expect
(process-key (make-world 0 INIT-LOA
'left E-LOS)
" ")
(make-world 0 INIT-LOA 'left (list (make-posn 0 MAX-IMG-Y))))
92 The process-key Refinement 357
(check-expect
(process-key (make-world 0 INIT-LOA 'left (list SHOT2))
"left")
(make-world 0 INIT-LOA 'left (list SHOT2)))
As before, these tests still explicitly illustrate how a key is processed.
Having refined process-key to integrate the world refinements it is fairly easy
to mistakenly believe the design and implementation are done. It is important to
implement all the changes made to the game. Recall that a shot must be added every
time the space bar is pressed by the player. This means that process-shooting
must always return a posn shot. In Aliens Attack 3, this function makes a decision
to return either 'NO-SHOT or a posn shot. Therefore, it must be refined to always
return an alien.
Given that a posn shot must always be returned by process-shooting, the
image-x value must be the given rocket value and theimage-y value must be
MAX-IMG-Y to put the shot at the bottom at the scene. This problem analysis leads
to these new sample expressions:
;; Sample expressions for process-shooting
(define PS-SHOT-VAL1 (make-posn INIT-ROCKET MAX-IMG-Y))
(define PS-SHOT2-VAL (make-posn INIT-ROCKET2 MAX-IMG-Y))
Abstraction over the sample expression yields the new function definition:
;; shot rocket → shot
;; Purpose: To create a new shot
(define (process-shooting a-rocket)
(make-posn a-rocket MAX-IMG-Y))
The new tests may be written as follows:
;; Tests using sample computations for process-shooting
(check-expect (process-shooting INIT-ROCKET) PS-SHOT-VAL1)
(check-expect (process-shooting INIT-ROCKET2) PS-SHOT2-VAL)
(define AFTER-TICK-WORLD2
(make-world
(world-rocket INIT-WORLD2)
(remove-hit-aliens
(move-loa (world-aliens INIT-WORLD2)
(world-dir INIT-WORLD2))
(move-los (world-shots INIT-WORLD2)))
(new-dir-after-tick
(remove-hit-aliens
(move-loa (world-aliens INIT-WORLD2)
(world-dir INIT-WORLD2))
(move-los (world-shots INIT-WORLD2)))
(world-dir INIT-WORLD2))
(remove-shots (move-los (world-shots INIT-WORLD2))
(move-loa (world-aliens INIT-WORLD2)
(world-dir INIT-WORLD2)))))
Observe that the only function not yet designed is new-dir-after-tick. For
now, we assume this function exists and we will design it later. The input to
new-dir-after-tick is the given list of aliens for the world being built as in-
put. That is, the moved list of aliens with the hit aliens removed. This is following
the design, as mentioned above, of Aliens Attack 3.
Abstracting over the sample expressions yields the specialized definition template:
;; world → world
;; Purpose: Create a new world after a clock tick
(define (process-tick a-world)
(make-world (world-rocket a-world)
(remove-hit-aliens
(move-loa (world-aliens a-world)
(world-dir a-world))
(move-los (world-shots a-world)))
(new-dir-after-tick
(remove-hit-aliens
(move-loa (world-aliens a-world)
(world-dir a-world))
(move-los (world-shots a-world))))
(world-dir a-world))
(remove-shots (move-los (world-shots a-world))
(move-loa (world-aliens a-world)
(world-dir a-world)))))
360 16 Aliens Attack Version 4
The final step is to specialize the tests. The tests using sample computations do not
change as they do not directly refer to a world. We use the same tests using sample
values as in Aliens Attack 3, but these tests are specialized to use the new world
data definition:
;; Tests using sample values for process-tick
(check-expect
(process-tick (make-world INIT-ROCKET
(cons (make-posn 1 5) '())
'left
E-LOS))
(make-world INIT-ROCKET
(cons (make-posn MIN-IMG-X 5) '())
'down
E-LOS))
(check-expect
(process-tick (make-world
INIT-ROCKET2
(list (make-posn (- MAX-CHARS-HORIZONTAL 2)
10))
'right
(cons SHOT2 '())))
(make-world INIT-ROCKET2
(list (make-posn MAX-IMG-X 10))
'down
(cons (move-shot SHOT2) '())))
This function must decide for the given list of aliens if the next direction is right or
left. The needed conditional must determine if the given loa has an alien at the right
edge or at the left edge. The assumption is made that the given loa does not have
aliens at both edges. Without loss of generality, we choose to determine if the given
loa contains an alien at the left edge. To do so we use any-alien-at-left-edge?
designed in Sect. 74.1.
The design of the function is straightforward and its implementation is
;; loa → direction
;; Purpose: Compute the direction of the given alien
;; when previous direction is down
;; ASSUMPTION: The given loa does not have aliens at
;; both edges
(define (new-dir-after-down a-loa)
(if (any-alien-at-left-edge? a-loa) 'right 'left))
This function must decide for the given list of aliens if the next direction is down or
left. The needed conditional must determine if the given loa has an alien at the left
edge. If so, the next direction is down. Otherwise, it is left. To do so we, once again,
use any-alien-at-left-edge? designed in Sect. 74.1.
364 16 Aliens Attack Version 4
This function must decide for the given list of aliens if the next direction is down
or right. The needed conditional must determine if the given loa has an alien at the
right edge. If so, the next direction is down. Otherwise, it is right. To do so we use
any-alien-at-right-edge? designed in Sect. 74.2.
The design of the function is straightforward and its implementation is
;; loa → direction
;; Purpose: Compute the direction of the given loa
;; when previous direction is right
(define (new-dir-after-right a-loa)
(if (any-alien-at-right-edge? a-loa)
'down
'right))
Ask yourself when should the game be over in Aliens Attack 4. When does the player
lose the game? The player loses when any alien reaches earth. A predicate to detect
this condition, any-alien-reached-earth?, is designed in Sect. 74.3. When does
the player win the game? The player wins when there are no more aliens left.
Based on our problem analysis we can define a world instance and write sample
expressions as follows:
(define WORLD4 (make-world 4 '() 'left '()))
(define GAME-OVER2
(or (any-alien-reached-earth?
(world-aliens WORLD4))
(not (any-aliens-alive?
(world-aliens WORLD4)))))
(define GAME-NOT-OVER
(or (any-alien-reached-earth?
(world-aliens INIT-WORLD))
(not (any-aliens-alive?
(world-aliens INIT-WORLD))))))
366 16 Aliens Attack Version 4
The sample world is defined to facilitate writing the sample expressions. The first
sample expression computes the answer for a world having an alien that has reached
earth. The second sample expression computes the answer for a world that has no
aliens. The third sample expression computes the answer using a world for which
the game is not over. Determining whether there are aliens left in the world, just like
any-alien-reached-earth?, is a loa problem. In the interest of separation of
concerns, a loa-processing function is needed.
Abstraction over the sample expressions yields
;; world → Boolean
;; Purpose: Detect if the game is over
(define (game-over? a-world)
(or (any-alien-reached-earth? (world-aliens a-world))
(not (any-aliens-alive? (world-aliens a-world)))))
The tests for the function are
;; Tests using sample computations for game-over?
(check-expect (game-over? INIT-WORLD2) GAME-OVER1)
(check-expect (game-over? WORLD4) GAME-OVER2)
(check-expect (game-over? INIT-WORLD) GAME-NOT-OVER)
The final refinement involves the function used to draw the last world. Recall that
this function is used only when game-over? returns #true. How do we determine
if the player has won or lost? We may use any-alien-reached-earth? and
any-aliens-alive? to determine this. Based on this problem analysis we may
write sample worlds and sample expressions as follows:
;; Sample Instance of (final) world
(define FWORLD1 (make-world 13
(list (make-posn 0 MAX-IMG-Y))
'right
E-LOS))
(define FWORLD2 (make-world 7
(list (make-posn 19 MAX-IMG-Y))
'left
(list (make-posn 2 4))))
(define FWORLD3
(make-world 7 '() 'left (list (make-posn 19 3))))
)
(check-error
(draw-last-world
(make-world 10
(list (make-posn 3 3) (make-posn 7 8))
'right
(list (make-posn 2 3))))
"draw-last-world: Given world has 2 aliens and none have
reached earth.")
The tests using sample values test a final and a non-final world. For the non-final
world, as in Aliens Attack 3, check-error is used to check the error thrown.
To complete the design run the program. Make sure all the tests pass and play the
game several games. Does it all work as expected? Make sure that you play the game
several times before proceeding.
After playing the game several times you may or may not have noticed that there is a
bug. If you did not notice it, do not be concerned. This is a bug that does not always
manifests itself. A special set of conditions must occur to be able to see the bug. To
illustrate the bug define the following world:
(define BUG-WORLD (make-world
10
(list
(make-posn 4 6)
(make-posn 10 6)
(make-posn 11 6)
(make-posn 12 6)
(make-posn 10 7)
(make-posn 11 7)
(make-posn 13 7)
(make-posn 15 8))
'left
(list (make-posn 0 12))))
370 16 Aliens Attack Version 4
Observe that there is an alien that is farther left than all the other aliens and that the
aliens are moving left. Now, edit the run function to use BUG-WORLD as follows:
; string → world
; Purpose: To run the game
(define (run a-name)
(big-bang BUG-WORLD
[on-draw draw-world]
[name a-name]
[on-key process-key]
[on-tick process-tick TICK-RATE]
[stop-when game-over? draw-last-world]))
Run the game several times and see if you can notice the bug. What do you notice
about how the aliens move?
Let us closely analyze what happens. In particular, we shall focus on the first
alien, the only shot, and the direction. The following table captures how these values
change as the clock ticks:
TICK ALIEN SHOT DIRECTION
0 (make-posn 4 6) (make-posn 0 12) 'left
1 (make-posn 3 6) (make-posn 0 11) 'left
2 (make-posn 2 6) (make-posn 0 10) 'left
3 (make-posn 1 6) (make-posn 0 9) 'left
4 (make-posn 0 6) (make-posn 0 8) 'left
5 (make-posn 0 7) (make-posn 0 7) 'down
The alien starts at image coordinates (4 6) and the shot starts at image coordinates
(0 12). The alien moves left and the shot moves up until tick 4. At tick 4 the alien
is at the left edge and the direction changes to down. At tick 5 the alien has moved
down and the shot has moved up. Observe that the shot hits the alien. Therefore,
process-tick removes both from the world. The new direction is computed by
new-dir-after-tick using the list of aliens that does not contain the hit alien and
'down. This function, in turn, calls new-dir-after-down. Pay close attention to
what new-dir-after-down does. It tests if any alien is at the left edge. This test
returns #false and the function returns 'left. The aliens go back to moving left
when they should move right. This means that we were not careful enough in our
original problem analysis. It is not perfectly reasonable to compute the new direction
using the moved and filtered list of aliens for the next world being built. Take note of
the fact that despite hundreds of tests passing there is still a bug in the program. This
ought to drive home an important lesson. Tests do not guarantee that a program is
correct. They only give us cautious optimism that it is correct. As problem solvers it
is our responsibility to make sure a program is correct. In a future course in Discrete
Mathematics, Program Correctness, or Formal Verification you will learn how to
prove programs correct.
95 A Bug Despite Hundreds of Tests Passing 371
** Ex. 179 — Define a world that manifests the bug when the aliens are moving
right.
For now, we must revisit our problem analysis to fix the bug. The problem in our
example is that the hit alien is removed before computing the new direction. If the
hit alien is not removed, then new-dir-after-down’s test returns #true and the
function returns 'right. That is, the returned direction is correct. Therefore, we
must refine process-tick to be
;; world → world
;; Purpose: Create a new world after a clock tick
(define (process-tick a-world)
(make-world (world-rocket a-world)
(remove-hit-aliens
(move-loa (world-aliens a-world)
(world-dir a-world))
(move-los (world-shots a-world)))
(new-dir-after-tick
(move-loa (world-aliens a-world)
(world-dir a-world))
(world-dir a-world))
(remove-shots
(move-los (world-shots a-world))
(move-loa (world-aliens a-world)
(world-dir a-world)))))
Observe that the new direction is computed without removing hit aliens. Run the
program and make sure all the tests pass. Enjoy the new version of the game!
***** Ex. 180 — PROJECT: Change the world and alien data definitions
to:
;; A world is a structure
;; (make-world rocket loa dir los natnum)
(define-struct world (rocket aliens dir shots ticks2shoot))
The player is limited to shoot every ticks2shoot tick. When a player shoots a
world with ticks2shoot equal to some natural number, a constant is created.
After every tick ticks2shoot is decreased by 1. The player may shoot again
when ticks2shoot is 0.
Aliens have a level of health representing how many times they must be hit
before destroyed. Every time an alien is hit its health is decreased by 1. When
an alien’s health is 0, it is removed. Pick an alien color scheme to indicate the
372 16 Aliens Attack Version 4
health of the alien. For example, if an alien starts with a health of 3, then at 3 it
is drawn black, at two it is drawn orange, and at 1 it is drawn red.
We have explored data of arbitrary size whose definitions only contain one selfref-
erence. For example, (listof X) and natnum only have one selfreference:
A list of X is either: A natural number is either:
1. '() 1. 0
2. (cons X (listof X)) 2. (add1 natnum)
There are, however, many data types that contain more than one selfreference.
Consider, for example, representing the results of an arbitrary number of coin flips.
Each time you flip a coin the result is either heads or tail. How can we represent all
the possible outcomes of flipping a coin an arbitrary number of times? Figure 77
displays a graphical representation of all possible outcomes for 3 coin flips. At the
top level, 0 heads and 0 tails have been flipped. To the left at the next level, after a
head is flipped, we have 1 head and 0 tails. To the right at the next level, after a tail
is flipped, we have 0 heads and 1 tail. You may confirm that going left represents
that a head is flipped and going right represents that a tail is flipped as you traverse
down to the next level.
The data representation in Fig. 77 is called a binary tree. A binary tree may be
empty. A non-empty binary tree is made up of 1 or more nodes. A node has data,
like the number of heads and tails flipped, and at most two children. The parent node
is connected to its children by an edge. If an edge goes left, it connects the parent
with its left child. If an edge goes right, it connects the parent with its right
child. The top node of a binary tree is called the root. A node that does not have
any children is called a leaf. In Fig. 77, the root is the node that contains H:0 T:0
and all the nodes in the bottommost level are leaves. Binary trees are commonly used
to efficiently access data contained in nodes and to represent data with a bifurcating
structure—a structure where placement of a node (e.g., left or right) is part of the
information being represented.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 373
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_17
374 17 Binary Trees
H:3 T:0 H:2 T:1 H:2 T:1 H:1 T:2 H:2 T:1 H:1 T:2 H:1 T:2 H:0 T:3
1 2
3 4 5 6
7 8 9 10 11 12 13 14
Like a list, a binary tree may contain any kind of data. As problem solvers we are free
to define that type of data each node contains. For example, Fig. 77 depicts a binary
tree of coin-tally, where a coin-tally is a structure with two numbers, and
Fig. 78 depicts a binary tree of natnum. You may have already observed that binary
trees are generic. The question now, of course, is how are binary trees defined. Let
us focus closely on Fig. 78. Every node contains a natural number and two edges.
What do the edges represent? Let us closely examine the root. The root is connected
to its left child that contains 1. In addition to 1, the left child also has two children
(containing 3 and 4). The left child is a binary tree. Let us examine the right child of
the root. It contains 2 and two children. It is also a binary tree. How about the node
containing 12? It has 12 and two children (both are empty). We can now define a
binary tree of natural numbers:
;; A binary tree of natural numbers, (btof natnum), is either:
;; 1. '()
;; 2. (list natnum (btof natnum) (btof natnum))
Observe that we arbitrarily chose to represent the empty (btof natnum) as the empty
list and the non-empty btnatnum as a non-empty list. This may seem awkward for
the non-empty (btof natnum) given that there are always three elements: a natnum
and two (btof natnum). A structure would seem to be a better fit. We shall revisit
this issue in Chap. 18. Also observe that the circularity in the recursive definition
is useful. There are two varieties. One is not recursive and the other is recursive.
97 Binary Tree Data Definition 375
The recursive variety, however, has two selfreferences not one like a list or a natural
number. What does this tells us? It tells us that solving a problem involving a (btof
natnum) may require recursive calls to solve the same problem for the left subtree
and the right subtree.
Let us define a binary tree of coin tallies. In Fig. 77 we see the same structure as
in Fig. 78. This tells us that the data definitions will be similar. We define a binary
tree for coin tallies as follows:
;; A cointally is a structure: (make-cointally natnum natnum)
(define-struct cointally (heads tails))
;; (btof X) . . . → . . .
;; Purpose: . . .
(define (f-on-btx a-btx . . .)
(if (empty? a-btx)
...
. . .(f-on-X (first a-btx))
(f-on-btx (second a-btx))
(f-on-btx (thrid a-btx)). . .)))
376 17 Binary Trees
The structure of a list must be traversed from its first element to the last element.
Unlike a list, there are different ways a binary tree may be traversed. In this regard,
binary trees are similar to intervals. An interval may be traversed from its low-end
value to its high-end value and vice versa. Variety in the traversal possibilities offers
problem solvers flexibility in problem solving.
If we examine the definition template for a function on a (btof X), we see that
there is a call to a function on a X. This call processes the value at the root. There
are two recursive calls. These traverse the left and right subtrees to process all their
values. The template does not prescribe the order in which these calls are made.
That is, we are free to make these calls in any order depending on the problem
being solved and the design we choose to follow. Three of the basic strategies
are preorder traversal, inorder traversal, and postorder traversal.
A preorder traversal first processes the value at the root, then processes the left
subtree, and ends by processing the right subtree. An inorder traversal first processes
the right subtree, then processes the value at the root, and ends by processing the
left subtree. A postorder traversal first processes the left subtree, then processes the
right subtree, and ends by processing the value at the root.
98 Traversing a Binary Tree 377
Consider extracting all the strings from a (btof string). Given that binary
trees are data of arbitrary size, the returned value must also be data of arbitrary size.
A natural fit is to return a (listof string). Now reason in terms of the structure
of a binary tree. If the tree is empty, it contains no strings and the returned value
is the empty list. Otherwise, we must decide how to traverse the tree. The problem
statement does not specify the order in which the strings must be returned. Therefore,
we arbitrarily choose to return the list of strings following a preorder traversal of
the given binary tree. This means that the function adds the root string to the list
of strings that first contains the strings in the left subtree and then the strings in the
right subtree.
We define sample (btof string) as follows:
;; Sample instances of (btof X)
(define E-BTSTRING '())
(define BTSTRING2 (list
"Pokemon"
(list "Attack on Titan"
(list "Dragon Ball" '() '())
(list "One Piece" '() '()))
(list "Naruto"
(list "Death Note" '() '())
(list "Detective Conan" '() '()))))
The non-empty binary tree of strings represents a set of popular anime. The root of
the tree contains "Pokemon". Its left subtree is rooted at "Attack on Titan" and
its right tree is rooted at "Naruto". The leaves of the binary tree are the children of
the nodes containing "Attack on Titan" and "Naruto". Observe that the leaves
all have empty subtrees.
We formulate sample expressions as follows:
;; Sample expressions for max-btnatnum
(define EBTSTRING-VAL '())
(define BTSTRING2-VAL
(cons (first BTSTRING2)
(append (btstring-extract (second BTSTRING2))
(btstring-extract (third BTSTRING2)))))
As per our problem analysis, processing the empty (btof string) results in an
empty list of strings. If the given (btof string) is not empty, like BTSTRING2, the
right subtree’s list of strings in preorder is recursively computed. The same is done
for the left subtree. These two lists are made into one using append. To the front of
this list the string at the root is added. Observe that this is precisely a list of all the
strings in BTSTRING2 in preorder.
378 17 Binary Trees
The sample expressions provide us with the structure needed to specialize the
definition template as follows:
;; (btof string) → (listof string)
;; Purpose: Extract the strings in the given (btof string)
;; in preorder
(define (btstring-extract a-btstring)
(if (empty? a-btstring)
'()
(cons (first a-btstring)
(append (btstring-extract (second a-btstring))
(btstring-extract (third a-btstring))))))
The final step is to specialize the tests. As with all data types, we have tests using
sample computations and tests using sample values. The tests developed are:
;; Tests using sample computations for btstring-extract
(check-expect (btstring-extract E-BTSTRING) EBTSTRING-VAL)
(check-expect (btstring-extract BTSTRING2) BTSTRING2-VAL)
(check-expect (btstring-extract
(list "This"
(list "BT"
(list "is"
(list "like"
(list
"a"
(list "list." '() '())
'())
'())
'())
'())
'()))
(list "This" "BT" "is" "like" "a" "list."))
99 The Maximum of a (btof int) 379
The tests using sample values illustrate that the function works for binary trees that
are not full. That is, for binary trees containing leaf nodes at different levels or
containing nodes that have one subtree. Observe that the second of the tests using
sample values illustrates that a binary tree may have a structure similar to a list. This
occurs when either all left subtrees are empty or all right subtrees are empty.
Consider the problem of finding the maximum in a (btof int). For example, the
maximum of the (btof int) in Fig. 78 is 14. How did you determine this? You
probably scanned all the numbers in the tree. That is, you traversed the tree. Think
about what is needed to determine the maximum value and how the (btof int)
may be traversed to find the maximum. What does the root value need to be compared
to determine the maximum? It is not difficult to see that the root value needs to be
compared to the maximum of the left subtree and the maximum of the right subtree.
That means that a postorder traversal is a natural fit to this problem.
Now reason in terms of the structure of a (btof int). What is the answer if
the (btof int) is empty? If the given (btof int) is empty, then it contains no
maximum number. This informs us that we either need to define a subtype for a
non-empty (btof int) or the function must throw an error if it is given a empty
(btof int) as input. On this occasion, we design using the second option and have
the function throw an error. If the given (btof int) is not empty, then the function
needs to determine the maximum among the left subtree maximum, the right subtree
maximum, and the root value. Care must be taken because any of the subtrees or
both may be empty. A recursive call must not be made with an empty subtree to
avoid having the function throw and error. This means that there are 5 conditions that
must be tested for. For each, the value returned depends on the non-empty subtrees:
(btof int) Return
Empty Throw error
Leaf Root value
Left subtree empty Max of the root value and the right subtree max
Right subtree empty Max of the root value and the left subtree max
Neither subtree empty Max of root value, left subtree max, and right subtree max
380 17 Binary Trees
*** Ex. 181 — Provide a data definition for a non-empty (btof int). Design
and implement max-btint using your new data definition.
*** Ex. 184 — Design and implement a function to return the subtree in a
(btof int) that has the tree’s maximum as its root. If the root value is the
maximum value in the tree, then the entire tree is the subtree that has the tree’s
maximum as its root.
A natural way to represent the criminal database is to use a list. We may define a
criminal database and sample instances as follows:
;; A criminal database is a (listof cr).
;; Sample instances
(define CR-DB0 '())
(define CR-DB1 (list TEFLONDON BABYFACE SCARFACE
DILLINGER BUGSY))
Is this a good and efficient representation? Answering such a question is at the heart
of many problems. That is, it is not only important to solve a problem. It is equally
important to have a fast and efficient solution. Here is where representation plays
a significant role. A representation influences how a problem solver thinks about a
problem and how efficient a solution is.
Consider the problem of returning a cr in a database given an id number. To
solve this problem, as before, we think in terms of the structure of the database.
What is the answer if the database is empty? In this case, the wanted cr does not
exist. Therefore, we define the following cr to signal that the a cr with the given id
number does not exist:
(define DNE-CR (make-cr 0 "DNE" "CR DOES NOT EXIST"))
When the database is empty, the answer is DNE-CR. What is the answer if the database
is not empty? This means that there is at least a cr in the database. The id of the
first cr is compared with the given id. If they match, then the first cr is returned.
Otherwise, the rest of the database is recursively searched. The function to retrieve
a cr from a database represented using a (listof cr) is displayed in Fig. 79. The
function is designed using the template for a function on a (listof cr).
384 17 Binary Trees
Another representation option for the criminal database is to use a binary tree. The
idea here is to have a cr at the root and distribute the remaining crs among the
two subtrees. For example, we may define the same database from the previous
subsection as
(define CR-DB3 (list
DILLINGER
(list BABYFACE '() (list SCARFACE '() '()))
(list TEFLONDON (list BUGSY '() '()) '())))
DILLINGER is at the root of the tree and the rest of the criminal records are evenly
distributed among the subtrees.
Once again, consider how to retrieve a criminal record given an id number. Reason
in terms of the structure of a binary tree. If the given binary tree is empty, then the
answer is DNE-CR. What is the answer if the binary tree is not empty? In this case,
the tree must be traversed. It is reasonable to first check the root’s cr. Therefore, a
preorder traversal is appropriate. There are several cases that must be distinguished.
We start by checking if the root’s cr’s id matches the given id. If they match, then
the answer is the root cr and there is no need to traverse the subtrees. If they do not
match, then we traverse the left subtree first. The cr being searched for may or may
not be in the left subtree. How do we know if it is or it is not? Thinking about this
carefully reveals that we may determine the answer by examining the result obtained
by traversing the left subtree. If the result is DNE-CR, then the right subtree must be
100 Binary Search Trees 385
traversed for the answer. Otherwise, the answer is the result obtained from traversing
the left subtree.
The specialized template is displayed in Fig. 80. Observe that problem analysis
reveals 4 conditions that must be distinguished. There is a sample expression for
each. The first two sample expressions are for the nonrecursive cases, respectively,
the empty binary tree and CR-DB3’s root id matching the given id. The third sample
expression illustrates how to traverse CR-DB3’s right subtree when the left subtree
traversal returns DNE-CR. The fourth sample expression illustrates how to compute
the answer if CR-DB3’s left subtree contains the searched for cr.
Based on the problem analysis and the sample expressions the definition template
is specialized to have a conditional expression with 4 stanzas. The nonrecursive cases
are straightforward to understand. The recursive cases, on the other hand, need to
be scrutinized. As per our problem analysis, the left subtree is traversed first. It is
traversed, however, to determine if the result is DNE-CR and not to actually return a
result. As per our problem analysis, the right subtree is traversed if the traversal of the
left subtree is DNE-CR. If the result of the left subtree is not DNE-CR, then this result
is recomputed to return the answer. In other words, the function may traverse the
386 17 Binary Trees
left subtree twice. This does not seem very efficient. Why do the same work twice?
For small binary trees this may not matter. However, the repetition of a computation
may be costly if the given binary tree contains millions of criminal records.
Finally, observe that the first test using a sample computation searches the empty
database. The tests using sample values show that successful and unsuccessful
searches return the correct value.
Observe that the data definition lists the invariant properties. In this manner, it is
clear to all readers and problem solvers what a (bstof X) is. That is, (bstof X)
is a subtype of (btof X). Take note of the fact that the invariants define content
properties of a binary tree and do not affect the structure of a binary tree. Therefore,
the template of a function on a (btof X) is specialized when a binary search tree
problem is solved.
100 Binary Search Trees 387
The sample database used in the previous subsections may now be represented as
a binary search tree:
(define CR-DB4 (list TEFLONDON
(list BABYFACE
(list SCARFACE '() '())
'())
(list DILLINGER
'()
(list BUGSY '() '()))))
Observe that all the crs with ids less than the root’s id are in the left subtree and all
the crs with an id larger than the root’s id are in the right subtree. The same is true
for the subtrees rooted at BABYFACE, SCARFACE, DILLINGER, and BUGSY. In other
words, the invariant properties always hold and, therefore, CR-DB4 is a (bstof cr).
Given an id and a (bstof X), how do we search for the needed cr? We reason
about the structure of a (bstof X). That is, we reason about the structure of a
(btof X). If the (bstof X) is empty, once again, the answer is DNE-CR. If the
given id matches the id at the root, then the answer is the root cr. If the given id is
less than the root’s id, then the left subtree is recursively searched. Otherwise, the
right subtree is recursively searched.
Following the steps of the design recipe, we define a constant for the value of an
expression for each of the four conditions:
;; Sample expressions for get-record-bstocr
(define GRBST1-VAL DNE-CR)
(define GRBST2-VAL (first CR-DB4))
(define GRBST3-VAL (get-record-bstocr 23 (second CR-DB4)))
(define GRBST4-VAL (get-record-bstocr 675 (third CR-DB4)))
The sample expressions correspond, respectively, to searching a an empty (bstof
X), matching the root id number, needing to search the left subtree for an id number
less than the root id number, and needing to search the right subtree for an id number
greater than the root id number.
Based on the sample expressions we can specialize the definition template to be
;; number (btof cr) → cr
;; Purpose: To return the cr with the given id if it
;; exists in the given (bstof cr)
(define (get-record-bstocr id a-bstocr)
(cond [(empty? a-bstocr) DNE-CR]
[(= (cr-id (first a-bstocr)) id) (first a-bstocr)]
[(< id (cr-id (first a-bstocr)))
(get-record-bstocr id (second a-bstocr))]
[else (get-record-bstocr id (third a-bstocr))]))
388 17 Binary Trees
The stanzas for the nonrecursive cases appear first in the conditional. Observe that
both subtrees are never traversed. Based on the relation between the given id number
and the root’s id number only the left or the right subtree is traversed. Furthermore,
observe that neither subtree is ever traversed twice. Clearly, searching a (bstof X)
is more efficient than searching a (btof X) when the needed cr is in the left subtree.
The tests are written following the same motivating guidelines as the tests using
a (btof cr). The tests are
;; Tests using sample computations for get-record-bstocr
(check-expect (get-record-bstocr 899 CR-DB0) GRBST1-VAL)
(check-expect (get-record-bstocr 241 CR-DB4) GRBST2-VAL)
(check-expect (get-record-bstocr 23 CR-DB4) GRBST3-VAL)
(check-expect (get-record-bstocr 675 CR-DB4) GRBST4-VAL)
We have three different ways to search a criminal database based on three different
representations. Which one should we choose? The answer depends on the criteria
that must be met. If the most important criteria is code simplicity, then it is likely
that the representation using a (listof cr) is the best. Most of the time, however,
it is more important for a program to be efficient in terms of execution time, memory
usage, or both.
101 Abstract Running Time 389
Fig. 81 The function to build a list of random natural numbers less than 10,000,000
;; natnum (listof natnum<10000000)
;; Purpose: Create a list on n random natnums < 10000000
(define (build-random-list n)
(if (= n 0)
()
(cons (random 10000000) (build-random-list (sub1 n)))))
Let us focus on execution time. How can we describe the efficiency of a program?
In other words, how can we compare two programs to decide which one is more
efficient in terms of execution time? Clearly, we all dislike slow programs and want
all our programs to be fast. One way to measure efficiency is by timing the programs,
given the same input, that are being compared. In ISL+ we can time programs using
the time function. The time function takes as input an expression to evaluate and
returns its value. Before returning the value of the expression it prints information
about execution time in milliseconds including CPU time. For example consider
timing the sorting of a (listof natnum) using insertion sorting (from Sect. 78)
and ISL+’s built-in sorting function23 as follows:
(define LST1 (build-random-list 5000))
23
The second argument to sort tells the function to use < to compare numbers.
390 17 Binary Trees
Fig. 82 The predicate to determine if (listof natnum) members are all less than
10,000,000
;; (listof natnum) Boolean
;; Purpose: Determine if all natnums in the given list are in
;; [0..10000000)
(define (all<10000000 L)
(or (empty? L)
(and (<= 0 (first L) 9999999)
(all<10000000 (rest L)))))
At first glance, the collected data suggests that ISL+’s sort function is faster than our
sort-lon function. The most salient feature from the above table, however, is that
execution timing is an unreliable measure. For neither sorting function do we always
get the same result given the same input. Furthermore, the variation between different
runs is noticeable. In fact, if you run the same experiments on your computer, you
are likely to get different results also. The lesson here is that execution times may
vary on a computer and may vary among different computers. There must be a better
way to compare the expected execution time of programs.
In fact there is a better way to compare the expected execution time of programs.
Instead of counting milliseconds, we can count the number of operations performed
in relation to the size of the input. For example, we may count the number of
(recursive) function calls or the number of comparisons made. For a given input,
these do not vary among different runs nor among different computers. This means
that we expect a program that performs fewer of the counted operations to be faster
102 The Complexity of Searching the Criminal Database 391
regardless of the computer used. This is what is called abstract running time
or the complexity of a program. It is important to note that we do not count every
single operation done by the computer. Instead, we count abstract operations, like
recursive calls or comparisons, that may involve many computer operations. How
abstract operations are implemented may vary from one computer to another. Some
computer systems may use more or may use less computer operations to implement
an abstract operation. The number of abstract operations themselves does not vary
and that is what makes abstract running time a good basis for comparison when
evaluating the efficiency of a program. If a program performs n abstract operations
and on a given machine each operation performs k computer operations, then the
number of computer operations is k * n. That is, the number of computer operations
is proportional to some constant k. This constant of proportionality is different for
different computers and is ignored when determining abstract running time.
Typically, we are concerned with the worst-case scenario to establish the abstract
running time of a program. The number of abstract operations is described in terms
of the size of the input by a mathematical function that we must derive from the
code. If the size of the input is n, then we say that the complexity is the highest
power of n in this function. Big O notation is used to describe the complexity
of a program in terms of the input size. If a program performs 5n2 +2n+1 abstract
operations, we say the complexity of the program is O(n2 ), where n is the size
of the input. Observe that the constant of proportionality, the lower powers of n,
and constant factors are ignored. For example, consider the build-random-list
function in Fig. 81. If the input is 5, then there are five recursive calls performed for:
4, 3, 2, 1, 0. When n is 0, the recursion stops. Similarly, if the input is 3, then there
are three recursive calls performed for: 2, 1, 0. In general, the function is recursively
called n times. Thus, we say that the complexity of the function is O(n).
any id number in the database. This means that the entire database must be searched
to determine that a criminal record with the given id number does not exist, thus
making the complexity O(n). This tells us that the best and worst cases for both
representations (unsorted and sorted (listof cr)) are expected to have the same
performance. Neither is superior to the other.
Let us now consider the search complexity for the representation using a (btof
cr) from Sect. 100.2. In the best case get-record-btocr is called only once. That
is, in the best case the complexity is O(k). The worst-case scenario is searching for
an id number that is not in the tree. The left subtree is traversed to determine that
the left subtree does not contain the given id number and, subsequently, the right
subtree is traversed. This means that the number of calls to get-record-btocr is
equal to the number of nodes in the binary tree. Therefore, the search complexity is
O(n). This is the same complexity as using a (listof cr) and, therefore, using a
binary tree provides no improvement.
We now turn our attention to representing the database using a (bstof cr).
In the best case, as with the other representations, get-record-bstocr is only
called once and the search complexity is O(k). Analyzing the worst-case scenario
is more subtle. It occurs when a search is performed for an id number that is larger
(or smaller) than any id number in the binary search tree. A careless glance at the
code may suggest that at each step half of the criminal records are eliminated from
the search because only the left or the right subtree is traversed. Is it guaranteed that
half of the criminal records are eliminated from the search every time the function
is called? Consider, for example, the following database represented using a binary
search tree:
(define BST-LST
(list
PAULIE
'()
(list SCARFACE
'()
(list VITO
'()
(list BABYFACE
'()
(list TEFLONDON
'()
(list DILLINGER
'()
(list BUGSY '() '()))))))))
Observe that every left subtree is empty. This means that the structure of this binary
search tree is similar to the structure of a list. In an arbitrary binary search tree with
n nodes and the same type of structure searching for a cr with an id number greater
than anything in the database requires n calls to get-record-bstocr. Therefore,
in the worst case the complexity of this function is O(n). This means that all three
103 Balanced (bstof cr) 393
of our database representations have a searching function with the same complexity.
None are expected to always be better than any of the others.
Our efforts to date, regardless of the database representation, have yielded a linear
time searching algorithm. When using binary search trees, the problem is that in
the worst case only the record at the root is eliminated from the search, thus forcing
the examination of all records in the database. To overcome this problem we can
represent the database using a balanced binary search tree. A balanced binary tree
is one in which the height of the left and right subtrees of any node differs by no
more than 1. In other words, the binary tree is not lopsided and the descendants of
a node are roughly evenly distributed in its subtrees. We define a balanced binary
search tree as follows:
;; A (bbstof X) is either
;; 1. '()
;; 2. (list X (bbstof X) (bbtsof X))
;; SUCH THAT
;; A. All Xs in the left subtree are less than the root X
;; B. All Xs in the right subtree are greater than the root X
;; C. (<= (- (height left-subtree) (height right-subtree)) 1)
The third invariant guarantees that the nodes are evenly distributed and, therefore, at
each step of the search roughly half of the nodes left to explore are eliminated.
However, can a balanced binary search tree be created in the first place? Without loss
of generality, we assume that we have a (listof cr) sorted in non-decreasing order
by id number. The middle element of the list must be the root of the balanced binary
search tree. All the crs before the middle element must be placed in the left (balanced)
subtree and all the crs after the middle element must be placed in the right (balanced)
subtree. Accessing the middle element of the given list is fairly straightforward. If
the input list, a-locr, is not empty, then we know that its first element is indexed
by 0 and its last element is indexed by (sub1 (length a-locr)). Therefore,
the middle element is indexed by (quotient (length a-locr) 2). How then
are the elements before and after the middle element referenced? Observe that the
valid indexes into a-locr may be represented by an interval: [0..(sub1 (length
a-locr))]. This means that the elements before the middle element are indexed
by the interval elements in [0..(sub1 (quotient (length a-locr) 2))] and
that the elements after the middle element are indexed by the interval elements
394 17 Binary Trees
103.2 Analysis
Does representing a database as a balanced binary search tree improve the complexity
of searching? Consider the balanced binary search tree in Fig. 83 (that only depicts
id numbers). Consider searching for a cr record with an id of 87. After comparing
87 with 70 (the root id number) all the elements in the left subtree are discarded
from the search. That is, roughly half of the remaining elements are discarded. After
the comparison with 110 half of the remaining elements are discarded again from
the search. This happens again after the comparison with 90 and with 80. Finally,
after the comparison with 85 get-record-bbstocr is called with the empty tree
and the function halts. Notice that 6 calls are made to get-record-bbstocr. The
103 Balanced (bstof cr) 397
30 110
10 50 90 130
75 85 135 150
** Ex. 185 — Design and implement a function that takes as input a (bbstof
cr) and that returns the criminal record with the largest id number. Make sure
to do careful problem analysis to make the function as simple as possible.
*** Ex. 186 — Design and implement a function that takes as input a (bbstof
cr) and that returns its height.
*** Ex. 187 — Design and implement a function that takes as input a (bbstof
cr) and that returns a list of criminal names in order by id number.
*** Ex. 188 — Design and implement a function that takes as input a (bbstof
number) and that returns the tree’s second largest number.
**** Ex. 189 — Design and implement a predicate that takes as input a number
and a (bbstof number) and that determines if the given number is a member
of the tree. Also design and implement a predicate that takes as input a number
and a (listof number) and that determines if the given number is a member
of the list. Which do you expect to be faster? Justify your answer using abstract
running time.
**** Ex. 190 — Design and implement a predicate that takes as input a
(bbstof number) and that determines if all tree elements are even. Also
design and implement a predicate that takes as input a (btof number) and
that determines if all tree elements are even. Which do you expect to be faster?
Justify your answer using abstract running time.
So far, we have only considered data types that only refer to themselves. For example,
look at the data definitions for a (listof X) and a natnum:
A list of X is either: A natural number (natnum) is either:
1. '() 1. 0
2. (cons X (listof X)) 2. (add1 natnum)
These recursive data definitions only refer to themselves. Now, take a look at the data
definition for a binary tree:
;; A binary tree of X, (btof X), is either:
;; 1. '()
;; 2. (list X (btof X) (btof X))
This data definition also only refers to itself. However, as discussed in Chap. 17,
representing a non-empty binary tree as a list is a bit awkward given that it is not of
arbitrary size. A binary tree is of arbitrary size, but a non-empty binary tree always
has 3 elements: something of type X and two subtrees of type X. That is, the number
of elements needed to represent a non-empty binary tree is finite. This suggests that
a non-empty binary tree ought to be represented using a structure with three fields.
A first attempt to refine the binary tree data definition may be
;; A binary tree of X, (btof X), is either:
;; 1. '()
;; 2. A structure, (make-node X (btof X) (btof X)), with
;; an X value and two subtrees of X values
This data definition ought to feel more natural. An item of finite size is represented
using a structure and not a list.
This revised data definition, however, is still a bit awkward. We have a data
definition within a data definition. That is, we define a node inside the definition of
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 401
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_18
402 18 Mutually Recursive Data
a binary tree. It is much more natural to have distinct and separate data definitions,
one for a binary tree and one for a node, as follows:
;; A (nodeof X) is a structure, (make-node X (btof X) (btof X)),
;; with an X value and two (btof X) values.
Observe that there is something new about these data definitions. These data
definitions are mutually recursive. This means that they define intertwined data. The
data definitions refer to each other. A binary tree cannot be defined without knowing
what a node is and a node cannot be defined without knowing what a binary tree is.
105 Designing with Mutually Recursive Data 403
What impact does mutually recursive data have on problem solving? In a very real
sense we already know what references to a data definition mean for our code. For
example, in the data definition for a (list of X) there is a reference to a (listof
X). This means that we must call a function to process a (list of X). Given that
it is a selfreference it turns into a recursive call in our code.
The same principle applies to mutually recursive data definitions. The reference
to a node in the data definition for a (btof X) means that processing a non-empty
binary tree requires calling a function on a node. Similarly, the references to a (btof
X) in the data definition of a node require calls to a function that processes a (btof
X). Observe that the data definition for a (btof X) is still circular. A (btof X)
refers to node and a node refers (back) to (btof X). That is, it defines data of
arbitrary size. The definition is useful because the circularity ends when a (btof
X) is '(). These observations lead to the function templates and structure definition
displayed in Fig. 84. Immediately you can observe that the templates embody the
principle of separation of concerns. A node is processed by a node-processing
function and a (btof X) is processed by a (btof X)-processing function.
456 564
all the leaves are defined, all nodes that only have leaves as children may be defined.
This process continues until the root node is defined. In other words, we build the
instances needed from the bottom of the binary tree to the root of the tree level by
level. Consider defining the (btof int) displayed in Fig. 85. First, we define the
empty tree. Second, we define all the leaf nodes using the defined empty tree. Third,
we define binary trees rooted at the leaves. Fourth, we define nodes for the parents
of the leaves. Fifth, we define binary trees rooted at the parents of the leaves. Finally,
we define the root node and the corresponding binary tree. The definitions for the
binary tree in Fig. 85 are displayed in Fig. 86. The tree in Fig. 85 is BTI100.
Based on our sample instances we may write sample expressions for the maximum
of a (btof int) as follows:
;; Sample expressions for max-btint
(define BTI100-VAL (max-node BTI100))
(define BTI456-VAL (max-node BTI456))
105 Designing with Mutually Recursive Data 405
Observe that there is no sample expression for the empty (btof int) as this would
throw an error. For a non-empty binary tree a function to find the maximum of a
node is called. As usual, we assume that auxiliary functions exist and work.
These expressions lead to the following specialization of the definition template:
;; (btof int) → int throws error
;; Purpose: Return the maximum of the given (btof int)
(define (max-btint a-btint)
(if (empty? a-btint)
(error "An empty (btof int) has no maximum value.")
(max-node a-btint)))
Observe that this function is now much simpler and easier to read than the function
developed in Sect. 99. It is clear how the answer is computed. If the given binary tree
is empty, then there is no maximum integer. Otherwise, the maximum of the given
(btof int) is the maximum of a node.
The tests may be written as follows:
;; Tests using sample computations for max-btint
(check-expect (max-btint BTI100) BTI100-VAL)
(check-expect (max-btint BTI456) BTI456-VAL)
24
In fact, many other programming languages represent programs as symbolic expressions such as
LISP, Scheme, Racket, and BSL.
408 18 Mutually Recursive Data
;; Samples of slist
(define SLIST1 ’()) (define SLIST2 ...)
We first start by defining the following sample instances for the different data
types:
;; Sample instances of slist
(define SLIST1 '(* (+ 44 -44) (- 20 10)))
(define SLIST2 '(* 3 (+ 6 4)))
analyzing how to evaluate a sexpr. The evaluation of a number sexpr is the number
itself. The evaluation of an slist sexpr requires calling a function to evaluate an
slist. This analysis leads to the following sample expressions:
;; Sample expressions for eval-sexpr
(define SEXPR1-VAL SEXPR1) ;; for a number
(define SEXPR2-VAL (eval-slist SEXPR2)) ;; for a slist
Observe that the sample expressions illustrate how to implement our problem anal-
ysis.
The next step is to specialize the definition template and tests. This is accomplished
as follows:
;; sexpr → number throws error
;; Purpose: To evaluate the given sexpr
(define (eval-sexpr a-sexpr)
(if (number? a-sexpr)
a-sexpr
(eval-slist a-sexpr)))
;; slist → number
;; Purpose: To evaluate the given slist
(define (eval-slist an-slist)
(apply-f (first an-slist) (eval-args (rest an-slist))))
We may now proceed to design apply-f. This function takes as input a function
and a (listof number). It is necessary to determine the value of the given
function to properly combine the given numbers. The combination of the given
numbers involves one of three different tasks. Each of these tasks is performed by a
different auxiliary function. Therefore, the template for a function on a function is
specialized as follows:
;; Sample expressions for apply-f
(define APPLY1-VAL (sum-lon LON1))
(define APPLY2-VAL (subt-lon LON2))
(define APPLY3-VAL (mult-lon LON3))
;; lon number
;; Purpose: To multiply the numbers in the given lon
(define (mult-lon a-lon)
(if (empty? a-lon)
1
(* (first a-lon) (mult-lon (rest a-lon)))))
Therefore, we have
(- 1) = -1
(- 2 -4) = 2 - -4
(- 10 20 -5) = 10 - 20 - -5
What happens if - is provided no arguments? Try it in DrRacket. You can see that an
error is thrown. This is why eval-sexpr, eval-slist, eval-args, and apply-f
may throw an error. The function subt-lon is called by apply-f and, therefore,
calling apply-f may generate an error. In turn, apply-f is called by eval-slist,
which itself is called by eval-sexpr. Thus, these two functions may also throw
an error. Finally, eval-args may throw an error because it calls eval-sexpr. In
general, if a function f may lead to a call to function g that throws an error, then f
may also throw an error.
Now that we understand how to apply -, we can determine how to implement a
function to perform this task. If the given list of numbers is empty, then an error is
thrown. Otherwise, we need to subtract the rest of the numbers from the first number.
How can this be done? Observe that a - is placed before each number in the rest of
the list. By using a little Algebra we can factor out the -. For example, the last two
expressions above may be rewritten as follows:
(- 2 -4) = 2 - -4 = 2 - (-4)
(- 10 20 -5) = 10 - 20 - -5 = 10 - (20 + -5)
We can subtract from the first number the sum of the rest of the numbers.
414 18 Mutually Recursive Data
107 Trees
The concept of a binary tree may be generalized. Instead of each node having at
most two children, in a tree a node may have an arbitrary number of children. Figure
90 displays a tree representing moves on a Tic Tac Toe board.25 At level 0, the root
has 3 children. At level 1, the children of the root each have two children. At level 2,
the grandchildren of the root each have 1 child. Finally, at level 3 the nodes do not
have any children. As you can observe the number of children a node has is arbitrary.
How many children does the node representing the empty board at the beginning
of a game has? Clearly, such a node would have 9 children—one for each possible
move the first player may make.
Trees are versatile and may be used to represent many real or imaginary objects.
One use of trees is to represent a search space. A search space defines all possible
solutions to a problem and is searched to find a solution. For example, Fig. 90
represents all possible paths in a Tic Tac Toe gaming starting at the root node. Such a
tree, for instance, may be taken as input by a function that determines the next move
the computer ought to make.
25
Tic Tac Toe is played on a 3 × 3 board formed by using two vertical and two horizontal lines.
There are two players. One is designated 'X and the other 'O. The player’s goal is to get three
symbols in a row.
107 Trees 415
o x
o x o x o
x x o x x o x x o
o x o x o x x
o x o o x o o x o x o o o o
x x o x x o x x o x x o x x o x x o
o x o o x o x o o x o x x o x x
o x o o x o o o x o x x o o x o x o
x x o x x o x x o x x o x x o x x o
o x x o x x o x x o o x o x x o x x
We must now decide how to represent a search tree for Tic Tac Toe. Observe that
a search tree is made of nodes. Stop and think carefully what this means to a problem
solver. It suggests that two data definitions are needed: one for a node and one for a
search tree. Let us think carefully about a node. A node has a board (i.e., the current
state of the game) and a list of children. We use of list because the number of children
is arbitrary. Observe two more things. The first is the need for a third data definition:
a board. The second is that a node is of finite size and always contains two elements.
Now, let us think carefully about what a search tree is. What should the search tree
be if there is no game? We are free to choose any representation. Without loss of
generality, we shall say it is '(). If there is a game, then the search tree must be a
node. This data analysis suggests the following data definitions:
;; A node is a structure: (make-node board (listof st))
(define-struct node (board children))
;; A board is a structure:
;; (make-board bval bval bval bval bval bval bval bval bval)
(define-struct board (p0 p1 p2 p3 p4 p5 p6 p7 p8))
416 18 Mutually Recursive Data
;; (listof st) . . . → . . .
;; Purpose:
(define (f-on-lost an-lost)
(if (empty? an-lost)
...
. . .(f-on-st (first an-lost)). . .(f-on-lost (rest an-lost))))
;; node . . . → . . .
;; Purpose:
(define (f-on-node a-node)
. . .(f-on-board (node-board a-node)). . .
. . .(f-on-lost (node-children a-node)). . .)
;; Sample instances of ST
(define ST1 '())
(define ST2 . . .)
;; st . . . → . . .
;; Purpose:
(define (f-on-st an-st)
(if (empty? an-st)
...
(f-on-node an-st)))
A natural question that you may have is how to create a tree representing, for example,
the complete st for Tic Tac Toe starting from the blank board. To create an st we
need to know whose turn it is. Therefore, we create the following data definition and
sample instances:
;; A turn is either 'X or 'O
The function process-blanks returns a (listof st) containing one st for each
blank in the given board.
Abstracting over the sample expressions yields the following function:
;; board turn → st
;; Purpose: Create the st for the given board
(define (create-st a-board a-turn)
(make-node a-board
(process-blanks a-board
(find-blanks a-board)
a-turn)))
This function may be tested as follows:
;; Tests using sample computations for create-st
(check-expect (create-st BOARD1 'O) BOARD1-VAL)
(check-expect (create-st BOARD2 'X) BOARD2-VAL)
The test using a sample value is written using a board that only has a couple of moves
left to make it feasible to write the expected st.
*** Ex. 195 — Write the function find-blanks by specializing the template
for a function on a board. To do so, you may find the following data definition
useful:
;; A board position (bpos) is a natnum in [0..8]
The board positions may be numbered starting at the top left corner at 0 and
ending at the bottom right corner at 8. The function, find-blanks, must return
a (listof bpos).
*** Ex. 196 — Write model checking tests using boards that have a large num-
ber of moves left. What properties can you test about an st?
Based on the insights obtained from our problem analysis and sample expressions
we may write the function as follows:
;; board (listof bpos) turn → (listof st)
;; Purpose: Build the st for the given board and its given list
;; of blank positions
(define (process-blanks a-board a-lobpos a-turn)
(if (empty? a-lobpos)
'()
(cons (create-st
(place-on-board a-board (first a-lobpos) a-turn)
(if (eq? a-turn 'X) 'O 'X))
(process-blanks a-board (rest a-lobpos) a-turn))))
Observe that the if-expression is used to swap X to O and O to X for the call to
create-st. Finally, tests are written as follows:
;; Tests using sample computations for process-blanks
(check-expect (process-blanks BOARD1 '() 'X)
PROC-0BLANKS)
(check-expect (process-blanks BOARD3 '(1 2 5 6 7 8) 'O)
PROC-6BLANKS)
*** Ex. 198 — Write model checking tests for process-blanks. What prop-
erties can you test for a returned (listof st)?
422 18 Mutually Recursive Data
*** Ex. 199 — The design of create-st continues to place moves on boards
where one of the players has already won. Redesign create-st so that it does
not create children for such boards.
(define WIN-NODE3-VAL
(or
(and (not (has-win? (node-board WIN-NODE3)
(flip 'O)))
(has-win? (node-board WIN-NODE3) 'O))
(lost-can-win? (node-children WIN-NODE3) 'O)))
The sample expressions test if the given node’s board is a win for the given turn or
if any of the children lead to a win. The auxiliary function, flip, is used to change
an 'X to an 'O and vice versa. As expected, the children are processed by calling a
function on a lost.
Abstracting over the sample expressions leads to the following function definition:
;; node turn → Boolean
;; Purpose: To determine if the given turn can reach a win
(define (node-can-win? a-node a-turn)
(or (and (not (has-win? (node-board a-node) (flip a-turn)))
(has-win? (node-board a-node) a-turn))
(lost-can-win? (node-children a-node) a-turn)))
This function may be tested as follows:
;; Tests using sample computations node-can-win?
(check-expect (node-can-win? WIN-NODE2 'X) WIN-NODE2-VAL)
(check-expect (node-can-win? WIN-NODE3 'O) WIN-NODE3-VAL)
(check-expect (node-can-win?
(create-st (make-board 'O 'O 'O
'X 'X 'B
'X 'B 'B)
'X)
'X)
#false)
The tests using sample computations, once again, illustrate that the function computes
the values of the sample expressions. The tests using sample values provide examples
of nodes for which a win is possible and a win is not possible for the given turn.
107 Trees 425
The function lost-can-win? must determine if for a given turn any of its sts
lead to a win. If the given list is empty, then the answer is #false. Otherwise, a win
is possible if the first st leads to a win or any of the rest of the sts leads to a win.
Observe that a Boolean value is computed. This means we may opt to use a Boolean
function without using a conditional expression as suggested by the template for a
function on a lost. We may write sample instances and expressions as follows:
;; Sample instances of lost
(define WIN-LOST1 '())
(define WIN-LOST2 (node-children WIN-NODE2))
(define WIN-LOST3 (node-children WIN-NODE3))
(check-expect (lost-can-win?
(node-children
(create-st (make-board 'B 'B 'O
'O 'X 'O
'B 'X 'X)
'X))
'X)
#true)
The tests using sample values clearly communicate that both possible expected values
are properly computed. For the tests using sample values, any reader of our code
can easily observe that for the first test O cannot win and for the second test X can
win. As with all mutually recursive functions, place all function definitions before
the results of the other steps of the design recipe in order to run the program.
The goal of this project is for you to design and implement a Tic Tac Toe game. In
this game a player and the computer are opponents. The player is X and plays first.
The game initially displays an initial board similar to the one displayed in Fig. 91.
Feel free to embellish the game with graphics that you like. The player uses the
mouse to click on a board box to make a move. If it is the player’s turn and the
clicked box is blank, then an X is placed in the clicked box. After this the computer
makes a move by placing an O in any of the remaining boxes. The game cycles in
this manner until there is a winner or when there is a draw when all the boxes in the
board are not blank.
108 Project: Tic Tac Toe 427
To get you started here is the run function for the game:
(define (run a-name)
(big-bang INIT-WORLD
(on-draw draw-world)
(on-mouse process-mouse)
(on-tick process-tick)
(stop-when game-over? draw-last-world)
(name A-NAME)))
You need to follow a top-down design strategy starting with the event-handling
functions in the big-bang expression. The rest of the section will help you with
each of these.
There are two basic components that must be represented: the world and the image
for representing the world. Of these the easiest is the image of the world. This image
ought to include the game’s current board. For example, at the beginning of the
game the image rendered must include the initial board displayed in Fig. 91. If you
examine this image, you can see that the scene contains the board that has 9 tiles of
the same size and same color. You may find the following constants useful:
(define TILE-LEN 100)
(define TILE-CLR 'green)
(define WIDTH (* 3 TILE-LEN))
(define HEIGHT (* 3 TILE-LEN))
(define E-SCENE (empty-scene WIDTH HEIGHT))
428 18 Mutually Recursive Data
How can we represent the world? We can take advantage of the work we have
done in Sect. 107.1. As the game advances, we are stepping through the st that
has the initial board at the root. For a given st, the next board must be one of the
children. This child becomes the world’s st after each move. In addition, we need
to track the current turn to determine if the player is allowed to make a move. This
analysis leads the following data definition:
;; A world is a structure: (make-world st turn).
(define-struct world (st turn))
Drawing the world requires drawing a board and any embellishments that you like
to personalize the game to your liking (e.g., whose turn it is). What board ought to
be rendered? In accordance with our data representation, this board ought to be at
the root in the world’s st.
In the universe API, a mouse event handler needs four inputs: a world, an x
coordinate, a y coordinate, and a mouse event. The given x and y coordinates
correspond to the position of the mouse in the scene when a mouse event happens.
There are several mouse events defined and you may read about them through the
Help Desk. Of interest for us is the "button-down" event. This occurs when the
player clicks. When the player clicks on a tile, an X is placed in the clicked tile if:
1. It is X’s turn.
2. The clicked box is empty (i.e., blank).
3. The mouse event is "button-down".
To do so, a new world must be created by choosing the appropriate child from the
world’s st. If any of the conditions above do not hold, then handler returns the given
world.
The above problem analysis suggests that at least two auxiliary functions are
needed. One is needed to convert the x and y coordinates into a bpos. Another is
108 Project: Tic Tac Toe 429
needed to search an st’s children for the child st that is rooted at a given board. In
all likelihood you may find it useful to use the place-on-board function left as an
exercise above.
* Ex. 204 — Create data definitions and function templates for, ttt-x and
ttt-y, valid x and y coordinates for the Tic Tac Toe game.
The computer ought to make a move when the clock ticks and it is O’s turn. If it is X’s
turn, then the given world ought to be returned. To make a move the program must
decide the move to make and then extract the right child st from the given world.
The natural question that arises is how does the computer make a move. Two
popular approaches to have a program make a move in a game are rule-based
moves and heuristic-based moves. In the rule-based approach there is a rule
that, given a board, decides what the next move ought to be. In the heuristic-based
approach there is a function that assigns a score to each possible board after a move
and the move with the best score is chosen. We shall pursue a rule-based approach for
our version of Tic Tac Toe. In follow-up courses, you shall study the heuristic-based
approach that is commonly used in Artificial Intelligence.
Think carefully about how you play Tic Tac Toe. It is fairly clear that if you can
win you make the winning move. Otherwise, you determine if you can block and do
so if possible. What if you cannot win or block? We need to decide what move the
program ought to make. A reasonable strategy may be to play the center box if it is
empty. Otherwise, play a blank corner. If all corners are taken, then play a middle
space. This analysis means that we have a compound function with 5 conditions:
(cond [(can-o-win? a-board) (make-o-win a-board)]
[(can-o-block? a-board) (make-o-block a-board)]
[(center-empty? a-board) (make-o-center a-board)]
[(any-corner-empty? a-aboard) (make-o-corner a-board)]
[else (make-o-mid a-board)])
430 18 Mutually Recursive Data
The moves with the highest priorities must be tested for first (i.e., win then block) to
guarantee they take precedence.
*** Ex. 208 — Design and implement a function to make a computer (i.e., O)
move based on the rule outlined above.
*** Ex. 209 — Design and implement a function to process a clock tick.
***** Ex. 210 — The rule to make a computer move outlined above does not
consider creating or preventing the player from creating a fork. A fork is a
board in which there are two winning moves. Redesign your function to make
a computer move to take into account forking.
The game is over if either player has won or if the board is full. A player has won if
there are three of the same symbols on any row, column, or diagonal. A board is full
if there are no 'Bs left in the board.
The function to draw the last world may simply be draw-world. A better design
indicates the winning player or a draw.
** Ex. 211 — Design and implement a function to detect when the game is
over.
* Ex. 212 — Design and implement a function to draw the last world by indi-
cating the winner or a draw.
• In the rule-based approach there is a rule that, given a board, decides what the
next move ought to be.
• In the heuristic-based approach there is a function that assigns a score to each
possible board after a move and the move with the best score is chosen.
• The use of heuristic functions is common in Artificial Intelligence.
Chapter 19
Processing Multiple Inputs of Arbitrary
Size
Our focus up to know has mostly been on the design of functions that consume
a single instance of data of arbitrary size. This chapter discusses how functions
that consume multiple inputs of arbitrary size are designed. To simplify this initial
discussion we shall focus on designing functions that consume two inputs of arbitrary
size. How do you design such a function?
We outline three basic properties that you may establish once problem analysis is
performed. These are:
• One input has the dominant role.
• The two inputs must be processed simultaneously.
• There is no clear relationship between the inputs.
If one of the inputs, say of type X, has a dominant role, then the function may be
designed using the template for a function on X. You were exposed to this idea in
Sect. 61 albeit to process data of finite size. If the two inputs must be processed at
the same time, then pick one of the inputs and design the function using the template
for a function on the type of the given input. If there is not a clear relationship
between the inputs, then you need to analyze all possible combinations of the inputs’
subtypes.
Imagine that ISL+ did not include a function, append, to append two given lists. If
the programming language you are using does not include a function, you need then
you must design your own. How you design and implement a function to append two
lists? How did the creators of ISL+ design the append function? We shall follow
the steps of the design recipe to determine the answer.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 433
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_19
434 19 Processing Multiple Inputs of Arbitrary Size
We always start with problem analysis. We are given, say, L1 and L2. Ask yourself
what the result of appending the two lists ought to look like. It is not difficult to see
that a new list must be created that has all the elements of L1 followed by all the
elements of L2. To add all the elements of L1 to the resulting list L1 must be traversed.
Do we need to also traverse L2? As L1 is traversed cons is used to created a list
that contains the first element of L1 and the result of appending the rest of L1 and
L2. Consider what occurs with the last element of L1. This element must be consed
with the result of appending the rest of L1, which is empty, and L2. What ought to
be the result of appending empty with L2? Since L1 (i.e., empty) has no elements,
nothing ought to appear before the elements of L2. In other words, L2 is the answer.
Observe that L2 does not need to be traversed and, therefore, L1 plays a dominant
role. This informs us that we ought to design this function around processing L1.
We shall be careful with the next step of the design recipe, data analysis, to make
sure the design is sound. The types of the two inputs may be defined as (listof
X) and (listof Y). Observe that the two inputs may or may not be of the same
type. They are the same type (not value) when X = Y. Otherwise, they are of different
types. The natural question to ask, therefore, is what is the type of the returned list?
If X Y, then the result is neither of type (listof X) nor (listof Y). We know,
however, that every element of the result list is either of type X or Y. This means
there is variety among the elements of the result list. To capture this variety we need
the following data definition:
An XY is either:
1. X
2. Y
Armed with this data definition, we can now write the returned type as (listof
XY). Note that this means that every element of the returned list is either an X or a Y.
It does not specify that all the X elements must appear before the Y elements. Such a
specification is done by the purpose statement of the function.
We can now specialize the template for a function on X to write a function to
append two lists. Start by writing sample instances for a (listof X) and (listof
Y). For example, we may write:
;; Sample instances of (listof X), where X = number
(define LON1 '())
(define LON2 '(10 20 30 40))
Following the design recipe, we next develop sample expressions for myappend
based on the varieties of (listof X):
;; Sample expressions for myappend
(define LON1-LOS1-VAL LOS1)
(define LON1-LON2-VAL LON2)
(define LOS2-LOS1-VAL (cons (first LOS2)
(myappend (rest LOS2) LOS1)))
(define LON2-LOS2-VAL (cons (first LON2)
(myappend (rest LON2) LOS2)))
Observe that we test every possible combination of inputs’ subtypes. As per our
problem analysis if the first given list is empty then the answer is the second given
list. Otherwise, a new list is created by consing the first element of the first given
list to the result of appending the rest of the first given list and the second given list.
Abstracting over the sample expressions yields the following function:
;; (listof X) (listof Y) → (listof XY)
;; Purpose: Create a new list with members of the first given
;; list followed by members of the second given list
(define (myappend L1 L2)
(if (empty? L1)
L2
(cons (first L1) (myappend (rest L1) L2))))
Observe that the signature states the return type as the one we developed as part of
data analysis. Further observe that the purpose statement specifies that a new list is
created with the elements of L1 followed by the elements of L2.
Finally, test may be written as follows:
;; Tests using sample computations for myappend
(check-expect (myappend LON1 LOS1) LON1-LOS1-VAL)
(check-expect (myappend LON1 LON2) LON1-LON2-VAL)
(check-expect (myappend LOS2 LOS1) LOS2-LOS1-VAL)
(check-expect (myappend LON2 LOS2) LON2-LOS2-VAL)
*** Ex. 213 — Design and implement a function that takes as input a natural
number, n, and a (listof X), L, and that returns a list with n copies of L.
*** Ex. 214 — Design and implement a predicate to determine if all the ele-
ments of a given list are members of a second given list. You may find the ISL+
function member? useful.
**** Ex. 215 — Design and implement a function that takes as input a (btof
X), btx, and a (listof X), L, of length 2 and that returns btx with all
occurrences of the first element of L substituted with the second element of L.
*** Ex. 216 — Design and implement a function that takes as input two natural
numbers and that returns the product of the two numbers. Your function may
not use * and must use +. Recall, for example, that 4 * 3 = 4 + 4 + 4.
Given that to multiply corresponding numbers the two lists must be of the same
size, the lists are either both empty or they are both not empty. What is the answer
if the lists are empty? There are no corresponding numbers to multiply. Therefore,
the result must be the empty list. If the two lists are not empty, then this is when the
product of first element of each list is consed to the result of multiplying the rest of
both lists. The function may be designed by specializing the template for a function
on a (listof number).
Based on our problem analysis we can write sample instances and sample expres-
sions as follows:
;; Sample instances of lon
(define ELON '())
(define HOURS-WORKED '(1 23 39 27))
(define HOURLY-RATES '(13 22 18 34))
(define QUANTITIES '(10 3 86 27 8))
(define PRICES '(80 50 5 10 20))
(define TOTALS
(cons (* (first QUANTITIES) (first PRICES))
(mlist (rest QUANTITIES) (rest PRICES))))
The first sample expression illustrates that the result is empty when the given lists
are empty. The two other sample expressions illustrate that for non-empty lists a new
list is constructed using the product of the first element of each list and recursively
processing the rest of both lists.
Abstracting over the sample expressions yields the following function:
;; (listof num) (listof num) → (listof num)
;; Purpose: Return a list with the products of corresponding
;; elements in the given lists
;; Assumption: The given lists are of the same length
(define (mlist L1 L2)
(if (empty? L1)
'()
(cons (* (first L1) (first L2))
(mlist (rest L1) (rest L2)))))
Observe that the assumption that the two lists are of the same size is clearly indicated.
This informs any reader of the code that the function is specifically designed for two
lists of the same size.
438 19 Processing Multiple Inputs of Arbitrary Size
* Ex. 217 — Design and implement a function that takes as input two (listof
number) and that returns a (listof number) containing the sums of corre-
sponding numbers in the given list.
** Ex. 219 — Design and implement a function that takes as input a list of first
names and a list of last names and that returns a list of full names.
**** Ex. 220 — Design and implement a function that takes as input two
(listof Boolean) and that returns a (listof Boolean) indicating if the
corresponding elements in the given lists are the same.
**** Ex. 221 — Design and implement a function that takes as input two
(listof number) and that returns an intertwined list of numbers. For example,
given '(1 3 5) and '(2 4 6) the function returns '(1 2 3 4 5 6).
It is not uncommon to have more than one complex input and not be able to identify
a relationship between the inputs. That is, neither input dominates the other and
the processing of the inputs is not synchronized. In such cases, it is necessary to
analyze each possible combination of the inputs’ subtypes. A conditional expression
is needed in which the number of conditions is equal to the number of possible input
subtype combinations. Each stanza in the conditional expression tests for a specific
combination of subtypes. For instance, consider a function that processes a (listof
X) and a natural number. Each of these types has two subtypes. Thus, there are
112 No Clear Relationship Between the Inputs 439
decision must be made to either create a new list using sl1’s or sl2’s first element.
The element chosen must be the smaller of the two. The recursive call, therefore, is
made with the rest of the list whose first element is added to the result and the other
list.
Let us now use the following instances of a (listof number) to develop our
design and implementation:
;; Sample instances of (listof number)
(define ELON '())
(define SL1 '(1 22 30))
(define SL2 '(13 22 108 346))
(define SL3 '(-89 -50 0 6 90))
(define SL4 '(-240))
We start by writing sample expressions for each of the possible subtype combinations.
If both given lists are empty, the answer is the empty list. We define a sample
expression as follows:
;; Sample expressions for merge
(define ELON-ELON-VAL '())
If only one of the given lists is empty, then the answer is the other list. We define
sample expressions as follows:
(define ELON-SL1-VAL SL1)
(define ELON-SL2-VAL SL2)
(define SL3-ELON-VAL SL3)
(define SL4-ELON-VAL SL4)
The first two sample expressions illustrate the answer when the first given list is
empty. The second two sample expressions illustrate the answer when the second
given list is empty. For the final combination (i.e., when both lists are not empty) we
need examples that make the first element of the result the first element of the first
given list and examples that make the first element of the result the first element of
the second given list. We may write the following sample expressions:
(define SL1-SL2-VAL (cons (first SL1) (merge (rest SL1) SL2)))
(define SL3-SL2-VAL (cons (first SL3) (merge (rest SL3) SL2)))
(define SL3-SL4-VAL (cons (first SL4) (merge SL3 (rest SL4))))
(define SL1-SL4-VAL (cons (first SL4) (merge SL1 (rest SL4))))
The first two sample expressions take the first element of the first given list and
the second two sample expressions take the first element of the second given list.
Observe that the recursive call is always made with the rest of the list whose first
element is added to the result and with the other list untouched. It ought to be clear
that the processing of the lists is not synchronized.
112 No Clear Relationship Between the Inputs 441
To write the function definition we abstract over the sample expressions for each
subtype combination. This process yields the following function:
;; (listof num) (listof num) → (listof num)
;; Purpose: Return a list sorted in nondecreasing order that
;; only contains all the elements of the given lists
;; Assumption: Given lists are sorted in nondecreasing order
(define (merge sl1 sl2)
(cond [(and (empty? sl1) (empty? sl2)) '()]
[(and (empty? sl1) (cons? sl2)) sl2]
[(and (cons? sl1) (empty? sl2)) sl1]
[else (if (<= (first sl1) (first sl2))
(cons (first sl1) (merge (rest sl1) sl2))
(cons (first sl2) (merge sl1 (rest sl2))))]))
Observe that when both given lists are not empty a conditional expression is used
to determine from which list to take the first of the result. Further observe that the
recursive call is always done with the rest of the list from which the first element is
selected and the other list.
All conditions must be tested including the two in the else stanza of the cond-
expression. The tests may be written as follows:
;; Tests using sample computations for merge
(check-expect (merge ELON ELON) ELON-ELON-VAL)
(check-expect (merge ELON SL1) ELON-SL1-VAL)
(check-expect (merge ELON SL2) ELON-SL2-VAL)
(check-expect (merge SL3 ELON) SL3-ELON-VAL)
(check-expect (merge SL4 ELON) SL4-ELON-VAL)
(check-expect (merge SL1 SL2) SL1-SL2-VAL)
(check-expect (merge SL3 SL2) SL3-SL2-VAL)
(check-expect (merge SL3 SL4) SL3-SL4-VAL)
(check-expect (merge SL1 SL4) SL1-SL4-VAL)
The tests using sample computations are organized in the same manner as the
conditional expression in merge. The first test is for when both given lists are empty.
The second and third tests are for when only the first given list is empty. The next
two tests are for when only the second given list is empty. The final four tests are
for when both given lists are not empty. Of these, the first two are for when the first
given list’s first element is used to construct the result. The last two are for when
the second given list’s first element is used to construct the result. The tests using
sample values are organized in the same order, but there is only a single test for each
possible condition.
442 19 Processing Multiple Inputs of Arbitrary Size
*** Ex. 222 — Design and implement a function that extracts the nth element
of a list. The elements of the list are numbered 0 to n-1. If there is no nth
element, then the function ought to throw an error. You may not use list-ref
in your solution.
* Ex. 223 — Design and implement a function that merges two lists of numbers
sorted in non-decreasing order.
*** Ex. 224 — Design and implement a function that consumes two (listof
Boolean) and intertwines them based on the first element of each list. If the
first elements are the same, then put the first element of the first list in the result.
Otherwise, put the first element of the second list in the result. If either given
input is empty, nothing more is added to the result. For example, given '(#true
#true #true #false #false) and '(#true #false #false) the result
is '(#true #true #true #true #false #false).
*** Ex. 225 — Design and implement a function that takes as input two
(listof number) and a Boolean and that returns an intertwined list of num-
bers. For example, given '(1 3 5) and '(2 4 6) the function returns '(1 2 3 4 5
6). The Boolean input is used to decide from which given list to add its first
element to the result. If the Boolean is true, take the first element from the first
list. Otherwise, take the first element from the second list. Do you prefer this
version of the function over the version that synchronizes the processing of the
lists? Why or why not?
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 445
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_20
446 20 Functional Abstraction
symbol) are used in the body of the abstract function. This abstraction step yields
the following function:
;; contains?: symbol (listof symbol) → Boolean
;; Purpose: Determine if the given list contains the
;; given symbol
(define (contains? a-symbol a-los)
(and (not (empty a-los))
(or (symbol=? (first a-los) a-symbol)
(contains? a-symbol (rest a-los)))))
We can now use the abstract function to refactor contains-laptop? and
contains-pen? as follows:
;; contains-laptop?: (listof symbol) → Boolean
;; Purpose: Determine if the given list contains 'laptop
(define (contains-laptop? a-los) (contains? 'laptop a-los))
Observe how much shorter and readable these functions are now. Furthermore,
observe that now there is only one recursive function instead of two. The benefit of
the abstract function goes even further. It can be used to write functions to determine
if a (listof symbol) contains 'book, 'keys, and 'money as follows:
;; contains-book?: (listof symbol) → Boolean
;; Purpose: Determine if the given list contains 'book
(define (contains-book? a-los) (contains? 'book a-los))
The goal for functional abstraction is to abstract over similar expressions in the body
of different functions with a similar number of parameters. The process followed to
abstract over contains-laptop? and contains-pen? may be generalized into the
following design recipe for functional abstraction:
Mark Differences Compare and mark the differences in the bodies of the functions.
Create the Abstraction Define an abstract function that has the same number of
parameters as the functions abstracted over plus a parameter for every difference
identified in the previous step. The body of the abstract function references the
parameters for the differences instead of the values found in the functions ab-
stracted over. Make sure the signature takes into account that the new parameters’
types may vary making the function generic.
Refactor Refactor the functions abstracted over to use the abstract function.
The first step asks you to identify the significant differences in the expressions
that constitute the bodies of the functions. This means you ought to identify different
values. It does not mean differences in function or variable names. The second step
asks you to define the abstract function. This requires you to pick a descriptive name
for the function. The parameter list must include a parameter for each difference and
a set of parameters representing the parameters in the functions abstracted over. The
third step asks you to refactor the bodies of the functions abstracted over to use the
new abstract function. The tests for these refactored functions unchanged.
To illustrate the design recipe for abstraction in practice consider computing the
slope of a secant line. A secant line crosses the graph of a function, f(x), at exactly
two points: (x1 , f(x1 )) and (x2 , f(x2 )). When the distance between x1 and x2
448 20 Functional Abstraction
is very small, the slope of the secant line may be used as an approximation for the
slope of f(x) at the midpoint between the two points. The slope of a (secant) line is
fairly easy to compute as you learned in your high school algebra course:
f x2 − f x1
m = x2 − x1
This formula informs us that to compute the slope of the secant line we need two
x-values. Figure 93 displays two functions, xˆ2-sec-slope and xˆ3-sec-slope,
to compute the slope of the secant line, respectively, for f(x) = x2 and f(x) =
x3 .26
Observe that xˆ2-sec-slope and xˆ3-sec-slope are very similar and, there-
fore, good candidates for abstraction. Following the steps of the design recipe for
abstraction we find that the only significant difference is the function that is ap-
plied to an x-value. In xˆ2-sec-slope sqr is used and in xˆ3-sec-slope cube
is used. There is something new to learn here. The differences are functions. This
means that functions must be data just like a natural number and a posn are data.
26
The design of a function to cube its input is found in Sect. 38.
115 Functions as Values 449
In programming languages that provide support for functions as data we say that
the language has first-class functions. Among other things this means that functions
may be passed as arguments to a function. ISL+ is one such language and, therefore,
we may proceed with the process of functional abstraction.
The next step of the design recipe is to create an abstract function. For our current
problem this means that the abstract function needs a parameter for the single
difference, say f, and two parameters for the two x-values (as xˆ2-sec-slope and
xˆ3-sec-slope have). To write this function, however, we need to denote a function
in the signature. This is accomplished by writing a signature for the type of function
the parameter denotes. In this case both f(x) = x2 and f(x) = x3 have the same
signature: number number → number. The result of this step of the design recipe
for abstraction is
;; (number → number) number number → number
;; Purpose: Approximate the slope of the secant line between
;; the given x-values for the given function
(define (f-sec-slope f x1 x2) (/ (- (f x2) (f x1)) (- x2 x1)))
Observe that parentheses are placed around function values in the signature to aid
readability. It makes it clear that f-sec-slope returns a number. The body of the
function looks the same as the bodies of xˆ2-sec-slope and xˆ3-sec-slope
except that references to sqr and cube are now references to the parameter f.
The third step of the design recipe has refactor xˆ2-sec-slope and
xˆ3-sec-slope to use f-sec-slope. This means, in essence, that each of these
functions must call f-sec-slope with the right arguments. In practical terms, this
means xˆ2-sec-slope must make the call with sqr and the x-values it gets as input
and xˆ3-sec-slope must make the call with cube and the x-values it gets as input.
The result of this step is
;; number number → number
;; Purpose: Approximate the slope of the secant line between
;; the given x-values for f(x) = x^2
(define (x^2-secant-slope x1 x2) (f-sec-slope sqr x1 x2))
importantly, however, is the fact that first-class functions push programmers against
the limits of what is possible. Chapter 22 discusses how a function may be returned
as the value of a function just like a string or a number may be returned. To directly
test such a function the returned function and another function must be proven
equivalent. As you will learn in a Formal Languages and Automata Theory course
or a Computability course, testing the equivalence of functions is an unsolvable
problem. That is, there is no solution to the problem of determining if two functions
are equivalent. It is for this reason that abstract functions are tested through the
functions that make use of them.
Chapter 13 discusses several common operations over lists. If you think about it for
a minute, it is not unreasonable to expect functions that perform the same operation
for different types of lists to be almost identical. If this is the case, then they are
good candidates for abstraction. Sometimes, however, the expressions in the bodies
of these functions may not be the same. In such cases code refactoring may be used
to make the functions almost identical before performing functional abstraction.
This section presents the abstraction over functions that perform common list-
processing operations. Specifically, this section looks at the operations outlined in
Chap. 13: list summarizing, list searching, list ORing, list ANDing, list mapping, list
filtering, and list sorting.
In Sect. 72 the following functions to sum and to compute the length of a list of quiz
grades are developed:
;; (listof quizgrade) → number
;; Purpose: To compute the sum of the given
;; (listof quizgrade)
(define (sum-loq a-loq)
(if (empty? a-loq)
0
(+ (first a-loq) (sum-loq (rest a-loq)))))
In the same section two different exercises ask you to design and implement a
function to compute the product of a list of numbers and a function to append the
strings in a list of strings. Your solutions likely look like this:
;; (listof number) → number
;; Purpose: Compute the product of the given (listof number)
(define (product-lon a-lon)
(if (empty? a-lon)
1
(* (first a-lon) (product-lon (rest a-lon)))))
;; loq → number
;; Purpose: To compute the length of the given loq
(define (length-loq a-loq)
(if (empty? a-loq)
0
(+ (constant-f1 (first a-loq))
(length-loq (rest a-loq)))))
Now, length-loq uses its first element. It is still, however, different from the other
3 functions. It applies a function to its first element and the other 3 functions use the
first element directly.
To perform functional abstraction we need all 4 functions abstracted over to apply
a function to the first element of the given list. That is, sum-loq, product-lon, and
append-los need to apply a function to their first list element. This function must
452 20 Functional Abstraction
return the value it gets as input. In Mathematics, such a function is known as the
identity function: f(x) = x. We can implement the identity function and refactor
the three functions as follows:
;; X → X
;; Purpose: Return the given input
(define (id an-x) an-x)
Armed with a clear understanding of the differences we may sketch the abstract
function as follows:
;; ? (? → ?) (? ? → ?) (listof ?) → ?
;; Purpose: Summarize given list
(define (accum base-val ffirst comb a-lox)
(if (empty? a-lox)
base-val
(comb (ffirst (first a-lox))
(accum base-val ffirst comb (rest a-lox)))))
Observe that the abstract function preserves the structure of the functions abstracted
over. That is, it uses the same type of expressions in its body. The question that
remains is the signature of the abstract function. The types of lists processed by the
functions abstracted over vary. Let us denote the type of list processed by the abstract
as (listof X). This yields the following partial signature:
? (? → ?) (? ? → ?) (listof X) → ?
If the elements of the given list are of type X, then the input to the function applied
to the first element of the list must be also of type X. Therefore, the partial signature
now is
? (X → ?) (? ? → ?) (listof X) → ?
Observe that the returned type of the functions abstracted over varies. Let us denoted
this type as Z making the partial signature:
? (X → ?) (? ? → ?) (listof X) → Z
Whenever the abstract returns a value, it must be of type Z. This means that the
base value and the value returned by the combining function must also be Z. These
observations allow us to make the partial signature:
Z (X → ?) (? ? → Z) (listof X) → Z
Observe that the result of the recursive call in the abstract function is the second
input to the combining function. Thus, the partial signature now is
Z (X → ?) (? Z → Z) (listof X) → Z
Observe that the type of value returned by the function applied to the first element
of the list varies in the functions abstracted over. Furthermore, this value is the first
input to the combining function. If we denote its type as Y, the signature for accum
is
Z (X → Y) (Y Z → Z) (listof X) → Z
Finally, note that the signature defines a generic function. That is, the types X, Y,
and Z are not known until the function receives its input. Just like functions have
parameters signatures also have parameters. For functions the parameters must be
declared. We shall adopt the same policy for signatures. To declare type parameters
454 20 Functional Abstraction
for signatures we shall write the type variables inside angled brackets before the
signature. The signature for accum is
<X Y Z> Z (X → Y) (Y Z → Z) (listof X) → Z
This signature informs any reader of our code that the types represented by X, Y, and
Z only become known when arguments are provided to accum.
The final step is to refactor the functions abstracted over to use the abstract
function. The result of this step is
;; (listof quizgrade) → number
;; Purpose: To compute the length of the given
;; (listof quizgrade)
(define (length-loq a-loq) (accum 0 constant-f1 + a-loq))
Take time to appreciate what has been achieved by using abstraction. The most
obvious achievement is that instead of having 4 recursive functions there is only
one recursive function (i.e., the abstract function). This makes the code shorter and
more elegant. Elegance in this case refers to using a common function to accumulate
(thus the name accum) the summarizing value being computed. It is also noteworthy
that tests do not have to be rewritten for the functions abstracted over. Go ahead
and run your tests to verify that they all pass. After refactoring, however, the sample
expressions no longer reflect the expressions abstracted over to write the bodies of
the functions. We are now using a new way (i.e., the abstract function) to compute
the same value.
The most important achievement, however, is that we now have a powerful
function to perform list-summarizing computations that may be directly used to
solve other problems. Figure 94 displays the solution to extracting the x-values
from a (listof posn). Observe that the sample expressions use accum. That is,
get-xs-lop is directly designed using our abstract function. There is no need to
design a recursive function because the needed recursion is done by accum.
*** Ex. 229 — Design and implement a nonrecursive function to make a copy
of a given (listof X).
*** Ex. 230 — Design and implement a nonrecursive function to find the max-
imum of a non-empty list of natural numbers.
456 20 Functional Abstraction
**** Ex. 231 — Design and implement a nonrecursive function to count the
number of strings that have a length less than 5 in a given list of strings.
**** Ex. 232 — Design and implement a nonrecursive function that only uses
accum as an auxiliary function to compute the average of a given non-empty
list of numbers.
*** Ex. 233 — Argue that the calls to accum in length-loq, sum-loq, and
append-lostr satisfy accum’s signature.
displayed in Fig. 95. Armed with this function nonxsublist-lon may be refactored
to
;; symbol (listof symbol) → (listof symbol)
;; Purpose: To return the sublist of the given list that starts
;; with the first symbol not equal to given symbol.
(define (nonxsublist-los x a-los)
(cond [(empty? a-los) '()]
[(not-equal? x (first a-los)) a-los]
[else (notxsublist-los x (rest a-los))]))
Observe that the structures of xsublist-lon and nonxsublist-los are now
the same and, therefore, are good candidates for abstraction. The only difference is
the predicate used to compare the first element of the list. The abstraction step results
in
;; (? → Boolean) ? (listof ?) → (listof ?)
;; Purpose: To return the sublist of the given list that starts
;; with first element that satisfies given predicate.
(define (pred-sublist pred x a-lox)
(cond [(empty? a-lox) '()]
[(pred x (first a-lox)) a-lox]
[else (pred-sublist pred x (rest a-lox))]))
Once again, care must be taken to write the signature of the abstract function. Observe
that the list type processed varies among the functions abstracted over. If we denote
this type as (listof X), the partial signature is
<X> (? ? → Boolean) ? (listof X) → (listof ?)
458 20 Functional Abstraction
Observe that the second argument to the given predicate is a list element. This means
the partial signature becomes
<X> (? X → Boolean) ? (listof X) → (listof ?)
Now observe that in the second line of the conditional the given list is returned. This
suggests that the return type for accum is (listof X) making the partial signature:
<X> (? X → Boolean) ? (listof X) → (listof X)
Finally, observe that the type of the first argument to the functions abstracted over
varies but is always the same as the list elements’ type. Furthermore, this argument
is also the first input to the given predicate. Thus, we may conclude that the signature
of pred-sublist is
<X> (X X → Boolean) X (listof X) → (listof X)
It is now possible to refactor xsublist-lon and nonxsublist-lon to
;; number (listof number) → (listof number)
;; Purpose: To return the sublist of the given lon that starts
;; with the first instance of the given number.
(define (xsublist-lon x a-lon) (pred-sublist equal? x a-lon))
Fig. 96 Function to return the sublist that starts with a given string
;; string (listof string) (listof string)
;; Purpose: To return the sublist of the given (listof string) that starts
;; with the first instance of the given string.
(define (strsublist-lostr str a-lostr) (pred-sublist string=? str a-lostr))
*** Ex. 234 — Design and implement a nonrecursive function that takes as
input a number and a (listof number) sorted in non-decreasing order and
that returns the list of numbers greater than the given number.
*** Ex. 235 — Design and implement a nonrecursive function that consumes
an x-value and a (listof posn) and that returns the sublist starting with the
first posn that has an x-value greater than the given x-value.
* Ex. 236 — Design and implement a nonrecursive function that takes as input
a negative number and a (listof natnum) sorted in nonincreasing order and
that returns the (listof natnum) greater than the given negative number.
Section 74.1 discusses the development of the following function to determine if any
alien in a (listof alien) is at the left edge:
;; (listof alien) → Boolean
;; Purpose: To determine if any alien is at scene’s left edge
(define (any-alien-at-left-edge? a-loa)
(and (not (empty? a-loa))
(or (alien-at-left-edge? (first a-loa))
(any-alien-at-left-edge? (rest a-loa)))))
460 20 Functional Abstraction
As an exercise you were also asked to write a function to determine if a given list of
numbers contains an even number. Your solution likely looked as follows:
;; (listof number) → Boolean
;; Purpose: To determine if the list of numbers contains an
;; even number
(define (has-even-lon? a-lon)
(and (not (empty? a-lon))
(or (even? (first a-lon))
(has-even-lon? (rest a-lon)))))
Observe that both functions are structurally the same and, therefore, candidates for
abstraction.
The only significant difference between the functions is the predicate that is
applied to the first element of the list. Performing the abstraction step leads to the
following function:
;; <X> (X → Boolean) (listof X) → Boolean
;; Purpose: Determine if any list element satisfies the
;; given predicate
(define (ormap-pred pred? a-lox)
(and (not (empty? a-lox))
(or (pred? (first a-lox))
(ormap-pred pred? (rest a-lox)))))
Observe the list input types for any-alien-at-left-edge? and has-even-lon?
are different. Therefore, the type of ormap-pred’s second parameter is (listof
X). Its first parameter must be a predicate that takes as input a list element of type
X and returns a Boolean. Thus, the type of ormap-pred’s first parameter is (X →
Boolean).
Refactoring any-alien-at-left-edge? and has-even-lon? yields
;; alien → Boolean
;; Purpose: Determine if he given alien is at the left edge
(define (alien-at-left-edge? an-alien)
(= (posn-x an-alien) MIN-IMG-X))
*** Ex. 239 — Section 43 defines a student. A student makes the Dean’s list
if their grade point average is 3.5 or above. Design and implement a nonrecursive
predicate to determine if there are any students on the Dean’s list for a given
(listof student).
*** Ex. 240 — Design and implement a nonrecursive predicate to determine
if there are any images with an area of more than 500 pixels in a given (listof
image).
In Sect. 75.1 the following predicate to determine if all the elements in a list of
numbers are even is designed:
;; (listof number) → Boolean
;; Purpose: Determine if the given list of numbers only has
;; even numbers
(define (all-even-lon? a-lon)
(or (empty? a-lon)
(and (even? (first a-lon))
(all-even-lon? (rest a-lon)))))
If you were asked to write a predicate to determine if all strings in a list of strings
had a length less than or equal to 5, your function definitions would look as follows:
;; string → Boolean
;; Purpose: Determine if the length of the given string is ≤ 5
(define (string-len<5? a-str) (<= (string-length a-str) 5))
Section 76 discusses the design of the following functions to apply a function to all
the elements of a list and return the list of obtained results:
;; (listof shot) → (listof shot)
;; Purpose: To move the given list of shots
(define (move-los a-los)
(if (empty? a-los)
E-LOS
(cons (move-shot (first a-los))
(move-los (rest a-los)))))
Refactoring the functions abstracted over to use the abstract function yields
;; los → los
;; Purpose: To move the given list of shots
(define (move-los a-los) (map-f move-shot a-los))
*** Ex. 248 — Section 76 discusses the design of move-alien. Can this func-
tion be implemented using map? Justify your answer.
*** Ex. 254 — Argue that the calls to map-f in move-los and
lengths-lostr satisfy map-f’s signature.
466 20 Functional Abstraction
Section 77 discusses the design of a function to extract the even numbers from a list
of numbers and has an exercise to extract the posn shots from a list of shots. Both
functions are
;; (listof number) → (listof number)
;; Purpose: Return a list of the even numbers in the given list
(define (extract-evens a-lon)
(cond [(empty? a-lon) '()]
[(even? (first a-lon))
(cons (first a-lon) (extract-evens (rest a-lon)))]
[else (extract-evens (rest a-lon))]))
;; los → los
;; Purpose: Return list of posn shots in the given list
(define (extract-posn-shots a-los) (filter-pred posn? a-los))
The elegance of the refactored functions is appreciated by those that understand the
abstract function. This abstraction is so powerful and useful that ISL+ provides this
abstract function and it is named filter. Now that you understand the abstraction,
go ahead and use filter in your programs.
* Ex. 255 — Design and implement a nonrecursive function to extract the odd
numbers from a list of numbers.
** Ex. 256 — Design and implement a nonrecursive function to extract the
multiples of 10 from a list of numbers.
*** Ex. 257 — Design and implement a nonrecursive function to extract the
prime numbers from a list of numbers. Your auxiliary function to determine if
a given number is prime may be recursive.
*** Ex. 259 — Design and implement a nonrecursive function to extract the
posns from a (listof posn) that are either on the x-axis or the y-axis.
*** Ex. 260 — Argue that the calls to filter-pred in extract-evens and
extract-posn-shots satisfy filter-pred’s signature.
Section 78 discusses insertion sorting. The following functions are designed to sort
a list in non-decreasing order:
;; number (listof number) → (listof number)
;; Purpose: Insert the given number in the given list to
;; create an list of numbers in non-decreasing order
;; ASSUMPTION: The given lon is sorted in nondecreasing order
(define (insert a-num a-lon)
(cond [(empty? a-lon) (list a-num)]
468 20 Functional Abstraction
** Ex. 261 — Section 43 defines the type student. Design and implement a
nonrecursive function to sort a list of students in non-decreasing order by grade
point average.
*** Ex. 263 — Design and implement a nonrecursive function to sort a list of
aliens in non-decreasing order by how high they are in a scene.
*** Ex. 264 — Argue that the calls to sort-pred in sort-lon, sort-lon>=,
and sort-loi satisfy sort-pred’s signature.
Consider the following functions to process an interval. The first computes the sum
of the squares of the integers in the given interval. The second computes the list of
strings representing the integers in the interval.
;; [int..int] → int
;; Purpose: Compute the product of the squares of the ints
;; in the given interval
117 Abstraction over Interval-Processing Functions 473
;; [int..int] → int
;; Purpose: Compute the product of the squares of the ints
;; in the given interval
(define (interval-product-sqrs low high)
(interval-accum-l2h 1 sqr * low high))
Once again, observe that the functions abstracted over have been greatly simplified.
This abstract function, like the others in this chapter, allows the programmers to
spend more time on problem solving and less time on mundane typing of repetitions,
which usually means fewer bugs in the software developed.
** Ex. 265 — Design and implement a nonrecursive function to sum the inte-
gers in a given interval.
*** Ex. 267 — ISL+’s random function takes as input, n, a nonzero natural
number and returns a random natural number in [0..n-1]. Design and imple-
ment a nonrecursive function that takes as input an interval, [i..j], and that
returns a list of random numbers less than 1000, one for each integer in the
given interval. For example, (generate-randoms 10 12) may return '(786 31 87).
As the use of auxiliary functions increases, the size of programs grows. You may
have already noticed in Aliens Attack, for example, that you have related functions
scattered all over your program’s file. This makes it difficult to understand a design
and to make refinements. It is therefore desirable to encapsulate the definitions in
our programs. Encapsulation means that we package together all related definitions
as a single piece of software. This allows any reader of our code to more easily
understand the design and makes the process of program refinement easier.
New syntax is required to encapsulate definitions. This new syntax is a local-
expression. In essence, a local-expression allows us to have definitions inside of
functions. In this manner auxiliary functions are defined inside the functions that
need them instead of being defined elsewhere (possibly far away) in our program.
As this chapter and Chap. 25 illustrate, the power of encapsulation has consequences
for design and for performance.
119 Local-Expressions
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 477
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_21
478 21 Encapsulation
Consider, for example, the function to move a (listof shot) designed for the
Aliens Attack game in Sect. 76.2:
;; los → los
;; Purpose: To move the given list of shots
(define (move-los a-los)
(if (empty? a-los)
E-LOS
(cons (move-shot (first a-los))
(move-los (rest a-los)))))
Revisiting this function makes us realize that the same one-input function,
move-shot, is applied to every shot in the given list. This suggest that the function
may be refactored and simplified using map(similar to the design in Sect. 116.5:
;; los → los
;; Purpose: To move the given list of shots
(define (move-los a-los) (map move-shot a-los))
Can any reader fully understand and can any programmer easily make refinements
to this code? To a certain degree yes, but not fully. Any refinement to move-los,
for example, is likely to involve changes to move-shot. Therefore, it is desirable to
encapsulate both of these functions into a single package. Using a local-expression
allows us to refactor the dispersed code to
;; los → los
;; Purpose: To move the given list of shots
(define (move-los a-los)
(local [;; shot → shot
;; Purpose: To move the given shot
(define (local-move-shot a-shot)
(cond [(eq? a-shot NO-SHOT) a-shot]
[else
(cond [(= (posn-y a-shot) MIN-IMG-Y)
NO-SHOT]
[else
(make-posn
(posn-x a-shot)
(move-up-image-y
(posn-y a-shot)))]))
(map local-move-shot a-los))
The body of move-los is now a local-expression. This expression locally defines
the auxiliary function local-move-los. The name of move-los has been prefixed
by local to highlight that it is now a locally defined function. Such prefixing is not
necessary in practice. The body of the local-expression is the body of the original
move-los. Now, move-los is a complete package containing all the functions
needed to move a list of shots.
119 Local-Expressions 479
;; natnum → natnum
;; Purpose: Add x to the given number
(define (add-x-y-and-z y) (+ x y z))
;; natnum → natnum
;; Purpose: Compute the factorial of the given number
(define (fact x)
(if (= x 0)
1
(* x (fact (sub1 x)))))
120 Lexical Scoping 481
We do not need to refactor this code to understand it. The declaration of DELTA as
10 has global scope. This declaration is shadowed by the local declaration of DELTA
inside the local-expression. According to Rule 3 above, inside this expression
DELTA is 100. This makes VAL 200. Given that VAL is returned in the body of the
local, the value of the local-expression is 200. The local declaration of DELTA is not
valid outside of the local-expression. Therefore, the reference to DELTA as the first
argument to + must be to a different declaration of DELTA. In this case, the closest
declaration of DELTA is the global one making +’s first argument 10. It is now also
clear, without refactoring, that the value of the sum is 210.
As a final example, consider the following program:
(define X 2)
(define Y 3)
As with any new type of expression it is important to understand when and how
to use a local-expression. There are four reasons to use a local-expression. The
first, as we begun to discuss above, is encapsulation. Use to a local-expression to
organize your code to package together related functions. The second is to make
code more readable. The third is to make the use of abstract functions possible. The
fourth is to eliminate multiple evaluations of the same expression.
The following subsections discuss each of these reasons in more detail. Keep
in mind that sometimes the use of a local-expression is subjective. For example,
what does it mean for code to be more readable? For some people the use of a
local-expression may make understanding the design of a function easier and for
others the same function is perfectly understandable without it. If you feel that a
local-expression makes your code more readable, then do not hesitate to use it.
121.1 Encapsulation
Consider the function draw-world from Aliens Attack (its design is discussed
in Sect. 91):
;; world → scene
;; Purpose: To draw the world in E-SCENE
(define (draw-world a-world)
(draw-los (world-shots a-world)
(draw-loa (world-aliens a-world)
(draw-rocket (world-rocket a-world)
E-SCENE))))
Observe that 3 auxiliary functions and a constant are used. Where are these functions
and constant defined? Clearly, they are defined elsewhere in the program that forces
any reader of the code to search for them. Further observe that the auxiliary functions
and the constant are not needed elsewhere in the program. Given that the functions
have been thoroughly tested, they are ideal for encapsulation along with the constant.
The definitions to encapsulate are E-SCENE, E-SCENE-COLOR (only used to define
E-SCENE in the program), draw-world, draw-loa, and draw-rocket. The main
function is draw-world.
The draft new code after encapsulation for draw-world is displayed in Figs. 97
and 98. Observe that in Fig. 97 all the related definitions identified are localized.
The interface with the rest of the program (e.g., using draw-world in the big-bang
expression inside run) is unchanged because the signature, purpose, and function
121 Using Local-Expressions 485
)
(check-expect (draw-world (make-world INIT-ROCKET (list INIT-ALIEN2)
DIR2 (list SHOT2)))
header of the new (global) draw-world implementation remain unchanged. Take some
time to think about the call to draw-world inside the local-expression. Is this a
recursive call? Why or why not? Remember the scoping rules. It is not a recursive call
because it is calling the local draw-world function. There are no sample expressions
or tests using sample computations in Fig. 98 because the local functions are not in
scope and, therefore, may not be used to write sample expressions. This highlights
the importance of testing before encapsulation.
Observe that draw-shot is an auxiliary function that is only needed by draw-los.
The same observation may be made for draw-alien and draw-loa. Respectively
encapsulating these functions yield these new implementations for draw-los and
draw-loa:
;; los scene → scene
;; Purpose: To draw the given los in the given scene
(define (draw-los a-los scn)
(local [;; shot scene → scene
;; Purpose: To draw the shot in the given scene
(define (draw-shot a-shot scn)
(if (eq? a-shot NO-SHOT)
scn
(draw-ci SHOT-IMG
(posn-x a-shot)
(posn-y a-shot)
scn)))
486 21 Encapsulation
that draw-ci cannot be local to, for example, draw-shot because it would be out
of scope for the remaining two functions. If left at the global level draw-ci would
be in scope for all three functions. This, however, leaves draw-ci outside of the
only package that needs it. Therefore, the best solution is to make draw-ci local
to draw-world. Figures 99 and 100 display the completed encapsulated version of
draw-world. Observe that the auxiliary functions only needed by draw-ci are also
locally encapsulated.
Take a moment to appreciate what has been accomplished. The code is now
organized in a manner that allows any reader (including yourself 6 months from now)
to easily find related functions. It also means that when a refinement is necessary
we know where the new tested functions must be placed. For example, if a score is
added to the world, then after testing you know that the draw-score function ought
to be local to draw-world.
*** Ex. 270 — Section 106 discusses the design of eval-sexpr. Encapsulate
this function and its auxiliary functions.
121.2 Readability
Section 106 discusses the design of the following functions to evaluate arithmetic
expressions using +, -, and *:
;; (listof sexpr) arrow (listof number) throws error
;; Purpose: To evaluate the sexprs in the given list
(define (eval-args a-losexpr)
(if (empty? a-losexpr)
'()
(cons (eval-sexpr (first a-losexpr))
(eval-args (rest a-losexpr)))))
;; slist → number
;; Purpose: To evaluate the given slist
(define (eval-slist an-slist)
(apply-f (first an-slist) (eval-args (rest an-slist))))
121 Using Local-Expressions 489
represents the first argument and the latter represents the rest of the arguments. With
this in mind, a local-expression may be used to refactor eval-args as follows:
;; (listof sexpr) arrow (listof number) throws error
;; Purpose: To evaluate the sexprs in the given list
(define (eval-args a-losexpr)
(if (empty? a-losexpr)
'()
(local [(define first-arg (first a-losexpr))
(define rest-args (rest a-losexpr))]
(cons (eval-sexpr first-arg)
(eval-args rest-args)))))
As you solve a problem remember that the goal of a program is not only to
produce a solution. An equally important goal is to clearly communicate how a
problem is solved. The use of local-expressions can be very useful to make clear
what expressions represent.
The same function, move-alien, is applied to every alien in the given list and a
list of the results is returned. This suggests using map to re-implement this function.
Recall, however, the signature for map:
(X → Y) (listof X) → (listof Y)
It expects a one-input function as input. Do you see the problem? Look at the
signature for move-alien:
alien direction → alien
It is a two-input function and, therefore, cannot be given as input to map.
Observe, however, that to move an alien the only thing that varies is the alien to
be moved. The direction is the same for all aliens. Given that only one thing varies,
this suggests that inside move-loa the moving of an alien may be implemented as a
one-input function. This function may be defined locally and given to map as input:
;; (listof alien) dir → (listof alien)
;; Purpose: To move the given list of aliens in the
;; given direction
(define (move-loa a-loa dir)
(local [(define (alien-mover an-alien)
(move-alien an-alien dir))]
(map alien-mover a-loa)))
Why is such refactoring possible? Note that the value of dir does not change in the
recursive call. Therefore, inside move-loa dir may be considered a constant value
and a simpler function may be defined that does not have dir as input. Instead, dir
becomes a free variable inside the new local function. In general, within a function
any variables that do not change from one recursive call to the next may be treated
as constants to define a local function in which these variables are free variables. In
this case, it makes it possible to refactor a two-input function to move an alien into
a one-input function in which the direction to move the alien in is a free variable.
A note of caution is in order at this time. The local-expression in move-loa can-
not be eliminated using the draft refactoring recipe presented in Sect. 119. The prob-
lem is that alien-mover has a free variable, dir, that only exists inside move-loa.
Therefore, alien-mover cannot simply be moved outside the scope of dir. We
shall discuss how to solve this problem in Chap. 22.
** Ex. 275 — Refactor move-posns to use map. Write and run tests to validate
your transformation.
;; number number (listof posn) → (listof posn)
;; Purpose: Move the posns by the given amounts
(define (move-posns deltax deltay a-loposn)
(if (empty? a-loposn)
'()
(cons (move-posn deltax deltay (first a-loposn))
(move-posns deltax deltay (rest a-loposn)))))
492 21 Encapsulation
;; number (listof item) result Purpose: Get last item < given price
(define (get-last a-price a-loi)
(cond [(empty? a-loi) ()]
[(< (item-price (first a-loi)) a-price)
(if (empty? (get-last a-price (rest a-loi))) (first a-loi)
(get-last a-price (rest a-loi)))]
[else (get-last a-price (rest a-loi))]))
Instead of evaluating the same expression multiple times to obtain the same value,
locally define a variable and only evaluate the expression once. In the remainder of
the code reference the local variable instead of evaluating the expression again.
Following this principle get-last is refactored to
;; number (listof item) → result
;; Purpose: Get last item < given price
(define (get-last a-price a-loi)
(cond [(empty? a-loi) '()]
[(< (item-price (first a-loi)) a-price)
(local [(define rest-result (get-last a-price
(rest a-loi)))]
(if (empty? rest-result)
(first a-loi)
rest-result))]
[else (get-last a-price (rest a-loi))]))
Observe that rest-result captures the value obtained from processing the rest of
the list of items. Therefore, this local variable may be used instead of processing the
rest of the list of items multiple times.
Is this type of refactoring nitpicking or can it have a significant impact on per-
formance? To answer this question let us identify the input that exhibits the worst
possible performance. This occurs when the price given to get-last is larger than
the price of all the items in the given list. In such a case there is always an item with
a smaller price in the rest of a non-empty list. Let us define a list of with all identical
items using interval-accum-l2h (from Sect. 117) as follows:
;; int → item
;; Purpose: Returns a doll item that costs 30
(define (make-doll-item n) (make-item "doll" 30))
(define LOIL
(interval-accum-l2h '() make-doll-item cons 0 19))
121 Using Local-Expressions 495
LOIL is a (short) list of 20 identical items. If we rename the second version of the
function as get-last2, we can time the two versions as follows:
(time (get-last 1200 LOIL))
(time (get-last2 1200 LOIL))
The results obtained for CPU time are
Function CPU Time
get-last 1547
get-last2 0
On your computer the measured numbers might vary, but it is undeniable that there
is a significant difference in actual running time in favor of the version that uses a
local-expression.
How is this explained? Why is the get-last2 much faster? To answer this let us
look at the abstract running time of both functions. To ascertain the abstract running
time for get-last let us visualize the beginning of the function calls made. These
calls are displayed in Fig. 102. The initial call to get-last generates two calls using
the rest of the given list. Each of these two calls generates two calls with the rest
of the given list. We can extrapolate that every call to get-last will generate two
more recursive calls until the list is empty. How many calls are made for a list of
length n? The binary tree of calls in Fig. 102 would have height n (remember that
the root of the binary tree is at height 0). Each node in the binary tree represents a
function call. The binary tree would be a full binary tree (i.e., all leaves are at level
n). Therefore, the number of calls is equal to the number of nodes in a full binary
tree. The following table illustrates the number of nodes in a full binary tree of height
h:
h Number of Nodes
0 0
1 3
2 7
3 15
4 31
.. ..
. .
496 21 Encapsulation
From the table we can extrapolate that a full binary tree of height h has 2h+1 − 1
nodes. This means that the number of function calls for a list of length n is 2n+1 − 1
making get-last O(2n ). That is, the amount of work done by get-last grows
exponentially as the length of the input list grows.
What is the abstract running time of get-last2? Observe that for every function
call there is at most only one recursive call generated. This means that the total
number of function calls for a list of length n is n. This makes get-last2 O(n). In
other words, the work done is proportional to the size of the list. This explains why
get-last2 is much faster than get-last.
The lesson you should walk away with is that evaluating the same expression mul-
tiple times may have a profound impact on abstract and actual running time. It is not
the case, however, that eliminating the repetitive evaluation of the same expression
always leads to better abstract running time. In fact, it may not even have a noticeable
impact on actual running time. Another thing worth noting is that a program that has
an exponential abstract running time is not a practical solution to a problem. The
amount of work done grows too quickly and makes the computation of a solution
unfeasible even for input of moderate size. In future courses in Computer Science
you will study techniques to approximate a solution using, for example, heuristics
that have a polynomial (not exponential) abstract running time.
*** Ex. 278 — The powerset of a set, S, is the set of all subsets of S, including
the empty set and S itself. A colleague has written the following function to
compute the powerset of a set (foolishly not all the steps of the design recipe
have been followed):
;; A set is a (listof X)
If you run the code the tests pass. However, your colleague believes there is
a bug in the code. The following use of the function seems to never return an
answer or simply takes too long to be of practical use:
(powerSet '(a b c d e f g h i j k l m n o p q r s
t u v w x y z))
** Ex. 279 — Design and write a function that takes as input an x-value and a
list of posns and that returns the last posn in the given list that has the given
x-value. If no such posn exists, then your function ought to return #false.
**** Ex. 280 — Your friend is studying Computer Science at a university that
does not teach students about program design. Your friend wrote the following
function that takes as input a (btof number) and then returns the maximum
between the sum of the left and right subtrees:
(define (bt-sum t)
(if (empty? t)
0
(+ (first t)
(first (rest t))
(first (rest (rest t))))))
You are horrified, of course, by such unreadable code that computes the value
of the same expression more than once. Redesign the code to make it more
readable and avoid evaluating expressions more than once by following the
steps of the design recipe.
Section 115 introduces the idea that functions are a type of value just like numbers,
strings, and images are types of values. It presents how functions may be passed
as arguments to other functions. This ability allows us to develop powerful abstract
functions like map, filter, and ormap. This chapter continues the exploration of
functions as data. Specifically, it explores how to create a function value and how
to return a function as the value of a function application. This ability has profound
implications for problem solving and program design. For example, it leads to
functions that compute functions and to object-oriented programming (as Chap. 25
discusses).
Why would you want to compute a function in the first place? The short answer is
that it is a common operation in human life. Consider, for example, the composition
of two functions as you have studied in high school Mathematics. You may recall
how function composition is typically written:
(f ◦ g)(x) = f(g(x))
This equation is stating that the composition of f and g provides the output of g
as input to f. Although it is more subtle and not very emphasized in a high school
Mathematics course, the composition of f and g is a function. If we have two
functions f and g, the above equation tells us how to build a new function that we
call the composition of f and g. For example, Fig. 103 displays two functions. One
adds four to its input and the other doubles its input. If you were asked to compose
add4 and double (i.e., write a function that adds 4 to the doubling of its input) and
to compose double and add4 (i.e., write a function that doubles the sum of its input
and 4), your code may look as displayed in Fig. 104.
Observe that in Fig. 104 add4-o-double and double-o-add4 are good can-
didates for abstraction. As you expect the differences are the composed functions.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 499
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_22
500 22 Lambda Expressions
Using the design recipe for functional abstraction yields the following abstract func-
tion and refactored functions:
;; (number → number) (number → number) number → number
;; Purpose: Return f(g(x))
(define (f-o-g f g x) (f (g x)))
;; number → number
;; Purpose: Add 4 to the double of the given number
(define (add4-o-double x) (f-o-g add4 double x))
;; number → number
;; Purpose: Double the sum of the given number and 4
(define (double-o-add4 x) (f-o-g double add4 x))
All tests pass after running them. The abstract function, however, is not quite what
we want. Observe that x is a parameter in the abstract function, but not a difference in
the original functions. We want f-o-g to return a function and not the value of said
function for some value x. This means x should not be a parameter in the abstract
function. In other words, we want an abstract function with the following signature
and purpose:
;; (number → number) (number → number) → (number → number)
;; Purpose: Return the composition of the given functions
(define (f-o-g f g) . . .)
123 Anonymous Functions 501
;; number number Purpose: Double the sum of the given number and 4
(define (double-o-add4 x) (double (add4 x)))
You probably suspect already that returning a function is easy. You simply need to
return the function you want just like you return a number or a string of your choice.
This is partially correct. Consider, for example, the following program:
;; number → (number → number)
;; Purpose: Return + if the given number is even.
;; Otherwise, return -.
(define (choose-f x) (if (even? x) + -))
(check-expect ((choose-f 4) 1 5) 6)
(check-expect ((choose-f 3) 1 5) -4)
The function choose-f returns a function. Specifically, it returns + if its input is
even and it returns - if its input is odd. Therefore, ((choose-f 4) 1 5) is (+
1 5) and ((choose-f 3) 1 5) is (- 1 5). The application of choose-f to a
number returns a function that is then applied to the given arguments.
502 22 Lambda Expressions
The above example works because it handles functions that exist and have a
name. This is why it is easy to return a function. If a function has a name, it may
be returned as the value of a function. In other words, a function name is simply a
variable whose value is a function. In contrast, we want f-o-g to return a function
that does not exist in the program or in the programming language and, of course,
does not have a name. Therefore, we need an expression that returns a function when
it is evaluated. Such an expression is a lambda-expression (or λ-expression). The
syntax for a λ-expression in ISL+ is
expr ::= (lambda (symbol+ ) expr)
::= (λ (symbol+ ) expr)
These two expressions mean exactly the same thing. To type a "λ" in DrRacket use
Ctrl-\ (hold down the control key and press the backslash key). In parenthesis, a
λ-expression has the keyword lambda or the symbol λ followed by a list of symbols
and an expression. The list of symbols are the parameters of the function and the
expression is the body of the function. For example, the following expression returns
a function that adds 10 to its input:
(λ (a-number) (+ a-number 10))
We may apply this function to a number just like any other function that takes as
input a number. Consider, for example, the following expression:
((λ (a-number) (+ a-number 10)) 25)
When this application expression is evaluated, the λ-expression returns a function
that has a parameter called a-number and that has a body that adds 10 to a-number.
This function is applied to 25 to obtain 35 (the value of the application expression).
In essence, a λ-expression allows us to create an anonymous function. That is a
function without a name. If we wish to give such a function a name, then we need to
use define. For example, the function above to add 10 to its input may be bound to
a name as follows:
(define add10 (λ (a-number) (+ a-number 10)))
It is now possible for you to understand that
(define (<name> <name>$^+$) expr)
is syntactic sugar for
(define <name> (lambda (<name>$^+$) expr))
In other words, define binds a variable to value. The value may be any valid value
in ISL+ including functions.
You should consider using a λ-expression on its own (i.e., without binding its
value to a variable) when:
• A new function is needed.
• The needed function is not recursive.
• The needed function is only used once.
124 Revisiting Function Composition 503
*** Ex. 282 — Write a λ-expression to create a function that returns the ab-
solute value of its input. Use it to compute the absolute value of 0, 22, and
-40.
**** Ex. 283 — Write a λ-expression to create a function that returns the
second largest value of three given numbers. Apply it to 87, 27, and 100.
*** Ex. 284 — Write a λ-expression to create a function that moves a shot for
Aliens Attack. Apply it to (make-posn 4 5) and to NO-SHOT.
Given add4 and double, we need two functions: (add4 ◦ double)(x) and
(double ◦ add4)(x). We can define these functions using a λ-expression as fol-
lows:
;; number → number
;; Purpose: Add 4 to the double of the given number
(define add4-o-double (λ (x) (add4 (double x))))
;; number → number
;; Purpose: Double the sum of the given number and 4
(define double-o-add4 (λ (x) (double (add4 x))))
Observe that using a λ-expression does not change the steps of the design recipe.
You can substitute these new definitions for the definitions in Fig. 104 and all the
tests will pass. Try it out!
504 22 Lambda Expressions
Observe that the two expressions are nearly identical. This strongly suggests
abstracting over the expressions. This abstraction step yields the following function:
;; (number → number) (number → number) → (number → number)
;; Purpose: Return f(g(x))
(define (f-o-g f g) (λ (x) (f (g x))))
We now have the correct abstraction. We have a function that given two functions
returns a function for the composition of the two functions. We can now refactor the
original definitions to use the abstraction to get
;; number → number
;; Purpose: Add 4 to the double of the given number
(define add4-o-double (f-o-g add4 double))
;; number → number
;; Purpose: Double the sum of the given number and 4
(define double-o-add4 (f-o-g double add4))
You can now add f-o-g to and substitute these two functions in Fig. 104. All the
test will pass. Try it out!
Take some time to ponder what has been accomplished. We can design functions
that compute functions. This means we can now use functions that we did not write.
We use functions that were computed for us. This is a powerful tool in your design
arsenal. A program can create specialized functions as needed. In fact, the abstraction
provided by f-o-g is so useful that it is provided by ISL+ as compose.
** Ex. 287 — In high school Mathematics you learned that an invertible func-
tion, f, maps a value from its domain to a value in its range. The inverse of f,
f-1 , maps the range value back to the domain value. This suggests that the com-
position of f and f-1 is the identity function (implemented as id in Sect. 116.1).
Write a program to test this hypothesis. Your different definitions for the identity
function and corresponding tests ought to look like this:
;; number → number
;; Purpose: Return the given number
(define id-f1 (make-id f1 f1−1 ))
;; number → number
;; Purpose: Return the given number
(define id-f2 (make-id f2 f2−1 ))
*** Ex. 288 — You have a software company that supports manufacturing
companies. The total costs of any manufacturing company is given by the sum
of its variable costs and its fixed costs. The variable cost is proportional to the
number of units produced each month, while fixed costs are the same every
month. For example, assume that for company C1 the cost per unit is $5 and
the fixed costs are $100 and that for company C2 the cost per unit is $3 and the
fixed costs are $75. The respective functions for the total costs of companies C1
and C2 may be written as follows:
;; number → number
;; Purpose: Return the total costs of C1
(define (total-costs-C1 units-produced)
(+ (* units-produced 5) 100))
;; number → number
;; Purpose: Return the total costs of C1
(define (total-costs-C2 units-produced)
(+ (* units-produced 3) 75))
As the number of companies you develop total costs functions for grows, you
discover that typing similar functions over and over is a waste of your time.
Abstract over the above functions to develop an abstract function that computes
a total cost function. Refactor the above functions to make use of your abstract
function.
Observe that there is still a lot of repetition among these two functions. The only
significant difference is the function used to draw either an alien or a shot. Ab-
stracting over these functions suggests creating a function that may be outlined as
follows:
;; (X → image) → ???
;; Purpose: ???
(define (draw-lox-maker draw-x) (. . .))
What should such a function return? This proposed abstract function only takes as
input a drawing function. This drawing function by itself is of little use without the
list of elements to draw and the scene to draw them into. This means that this list
and scene are still needed as input. If they are needed as input, then draw-lox must
return a function that consumes these inputs. The returned function must capture
the similarities in draw-loa and draw-los. With this understanding the abstraction
step yields
;; (X image → image) → ((listof X) image → image)
;; Purpose: Return a function to draw a (listof X)
(define (draw-lox-maker draw-x)
(λ (a-lox scn) (draw-lox draw-x a-lox scn)))
The contract states that this function takes as input a function that consumes an
X and an image and that returns an image (i.e., a drawing function). It returns a
function that consumes a (listof X) and an image and that returns an image (i.e.,
a function to draw the given list). The resulting function is obtained by evaluating
a λ-expression.27 Observe that draw-lox-maker returns a specialized list-drawing
function.
The abstract function draw-lox-maker is an example of a curried function.
A curried function is one that consumes part of its input and returns a function
that consumes the rest of the input. In this example, draw-lox-maker consumes a
drawing function and returns a function that consumes a list of elements to draw and
an image. The returned function is specialized using the value given as input. For
example, consider the result of refactoring the functions abstracted over:
;; loa scene → scene
;; Purpose: To draw the given loa in the given scene
(define draw-loa (draw-lox-maker draw-alien))
27
The body of draw-lox-maker could also be implemented using a local-expression that defines
the returned function.
508 22 Lambda Expressions
What are draw-loa and draw-los? Like any other function, we may plug-in the
given arguments to draw-lox-maker and substitute the result into the above defi-
nitions:
;; loa scene → scene
;; Purpose: To draw the given loa in the given scene
(define draw-loa (λ (a-lox scn)
(draw-lox draw-alien a-lox scn)))
*** Ex. 289 — You have a physicist friend who routinely writes functions to
scale a list of numbers as the following (pardon the physicist who does not know
about the steps of the design recipe):
(define (scale-by-2 L) (map (lambda (x) (* 2 x)) L))
She complains that programming is too tedious because she must write almost
the same code over and over again. Help your friend by writing a curried function
to scale a list of numbers. Refactor the above functions to show your friend how
all the list-scaling functions can be computed for her.
*** Ex. 290 — In Sect. 117 the following functions that accumulate a value by
processing an interval from low to high are developed:
;; [int..int] → (listof string)
;; Purpose: Construct the list of strings for the ints in
;; the given interval
(define (interval->lostr low high)
(interval-accum-l2h '() number->string cons low high))
;; [int..int] → int
;; Purpose: Compute the product of the squares of the ints
;; in the given interval
(define (interval-product-sqrs low high)
(interval-accum-l2h 1 sqr * low high))
510 22 Lambda Expressions
Observe the similarities between the two functions. Develop a curried function
that produces specialized functions to accumulate a value by processing an
interval from low to high. The specialized functions produced should only take
an interval as input.
You may have the impression that designing abstract functions is a lot of work. You
may feel that it always requires abstracting over similar functions and refactoring
the functions abstracted over. Without a doubt this is an effective way to write
more elegant and easier to maintain code. An abstract function, however, may be
directly designed just like any other function. This section explores two examples
that illustrate how this can be done. If an abstraction already exists in another
domain, it can be directly designed and implemented. The two examples explored
are computing a series and computing an approximation for π.
the sum of the first n elements of the sequence of natural odd numbers is equal to
n2 . We will not prove this here, but we will illustrate it with the following examples:
02 = 0
32 = 1 + 3 + 5
52 = 1 + 3 + 5 + 7 + 9
Mathematicians would never explicitly write out all the elements of the sum for two
reasons. The first is that it is too cumbersome. The second is that it is impossible to
do so when the sequence is infinite. Instead, they have created syntax to abstractly
capture and compactly write the idea of summing the terms of a sequence:
02 = 0
32 = Σi=0
2 (ith-odd i)
52 = Σi=0
4 (ith-odd i)
(i.e., the number of terms to add). If n is 0, it returns the 0th term. Otherwise, it
adds the first n terms using structural recursion. Observe that 1 is subtracted from
n to add the terms. This is done because the number of integers in [0..n-1] is n.
Given that recursion is needed to process a given natural number greater than 0, the
function that does this work must be locally defined.
It now becomes straightforward to define a function to compute n2 . All that is
needed is to give series the function ith-odd as input. The computed function
may be defined and tested as follows:
;; natnum → natnum
;; Purpose: Compute the square of the given number
(define n-square (series ith-odd))
*** Ex. 292 — Redesign series using the following contract for add-terms:
;; natnum>=1 → number
the series for cubes and test if the result is always a square. Here is a sample program
to do so:
;; natnum → natnum
;; Purpose: Return the sum of the first n cubes
(define sum-cubes (series (lambda (n) (* n n n))))
126.2 Approximating π
used to approximate π increases the approximation gets better. This requires experi-
menting with the function to make sure it converges. That is, the approximation error
gets smaller as the number of terms grows. The following tests achieve this goal:
(check-within (pi-series 500) 3.1415 0.01) ;; 3.139592
(check-within (pi-series 3000) 3.1415 0.001) ;; 3.141259
(check-within (pi-series 12000) 3.1415 0.0001) ;; 3.141509
The tests clearly illustrate that as the number of terms increases the accuracy of the
results gets better. The values returned by pi-series are written as comments so
that any reader can easily see how the approximation of π is converging.
In closing, developing your design and abstraction skills using λ-expressions and
first-class functions is important. They are becoming more prevalent across all pro-
gramming languages. Virtually all functional programming languages (e.g., ISL+,
Racket, Haskell, Clean, and F#) have λ-expressions and support first-class func-
tions. This support is now also found in object-oriented programming languages
(e.g., Java, Kotlin, and Scala). In fact, now you may even use λ-expressions in
Excel.
**** Ex. 293 — The product of the terms of a sequence from a to b whose
elements are produced by f is defined as follows:
b f (i) = f(a) * f(a+1) * f(a+2) * . . . * f(b-1) * f(b)
Πi=a
***** Ex. 294 — The number of ways to choose k elements out of a finite set
with n elements
n
is k n+1−i
k = Πi=1 i
n
Write a program to test this hypothesis. Do you suspect the Professor is right or
wrong?
*** Ex. 296 — The Mathematics Professor claims that the constant e, an irra-
tional number, may be approximated using the first n values of the following
infinite series:
e = Σi=0∞ 1
i!
This chapter illustrates the use of encapsulation, abstract functions, and λ-expressions
in the context of a larger piece of software. In essence, this chapter refactors Aliens
Attack from Chap. 16. The goal is to group together related functions and to stream-
line the implementation of functions. The refactored code for the video game will
have a set of constants needed by multiple functions or for testing, 6 global func-
tions, and tests for the global functions. The global functions needed are run and the
functions used in the big-bang expression in run’s body.
Technically, the functions used in the big-bang expression can be encapsulated
inside of run. This may be a perfectly reasonable choice for thoroughly tested
code. We abstain from doing so for two reasons. First, run becomes too large and
obfuscated. Second, as discussed in Part V, different components in a multiplayer
game may need a different subset of the global functions. Therefore, it is best not to
encapsulate them for now.
An outline of the desired refactored code for Aliens Attack is displayed in Fig. 105.
The code is organized in 9 sections: data definitions, constants that are needed by
more than a single function or test, structure definitions and sample instances, and
6 functions. No new data definitions nor changes to existing data definitions are
required for this new version of the video game. Therefore, these remain unchanged
and are not furthered discussed in this chapter. The constants that are only needed
by a single handler are encapsulated. The constants at the global level include those
that are needed by more than one handler or that are used in any handler’s test.
After the constants, the structure definitions and sample instances are written. In our
game we only have one structure definition: world. This is followed by the code
for each handler. The code for each handler includes the function definition, sample
expressions, and its tests. The results from the steps of the design recipe must still
be displayed for all global functions. The sample expressions and tests for auxiliary
functions are not included given that they are encapsulated. Finally, the program
ends with the refactored run function.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 517
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_23
518 23 Aliens Attack Version 5
Each code section is discussed in the remaining sections of this chapter. The
presented refactoring is based on the code presented in previous chapters. Quite a
bit of editing is required, but mostly the editing is straightforward: encapsulating
functions and simplifying definitions. There are a small number of testing updates
or removal (due to encapsulation). The test updates involve making sure that every
line of code is covered by the tests.
128 Constants
Two varieties of constants must be identified: those needed globally and those needed
locally. Global constants are defined to be those that are needed by more than one
global function or by the tests of a global function. All other constants ought to be
encapsulated. We organize the global constants in three broad categories: general
constants needed to define elements of the video game, constants to define images,
and constants to define an initial world.
128 Constants 519
The general constants needed to define elements of the video game include
those needed to define a scene, to define coordinates inside a scene, and NO-SHOT.
Examining the game’s code reveals the following general constants:
;; General Constants
(define MAX-CHARS-HORIZONTAL 20)
(define MAX-CHARS-VERTICAL 15)
(define IMAGE-WIDTH 30)
(define IMAGE-HEIGHT 30)
(define MIN-IMG-X 0)
(define MAX-IMG-X (sub1 MAX-CHARS-HORIZONTAL))
(define MIN-IMG-Y 0)
(define MAX-IMG-Y (sub1 MAX-CHARS-VERTICAL))
(define E-SCENE-W (* MAX-CHARS-HORIZONTAL IMAGE-WIDTH))
(define E-SCENE-H (* MAX-CHARS-VERTICAL IMAGE-HEIGHT))
(define NO-SHOT ’no-shot)
Observe that no changes are made to any of these constants. Each of these constants
is either needed by more than one function or test or is needed by another constant
definition.
The constants needed to define images that must remain global are those used only
to test the draw-world function. Image constants used while the game is running
are encapsulated in draw-world. The global image constants used in testing are
;; Drawing Testing Constants
(define SHOT-COLOR2 'skyblue)
(define ALIEN-COLOR2 'orange)
(define WINDOW2-COLOR 'white)
(define FUSELAGE2-COLOR 'orange)
(define NACELLE2-COLOR 'brown)
(define ALIEN-IMG2 . . .)
(define SHOT-IMG2 . . .)
(define FUSELAGE2 . . .)
(define WINDOW2 (ellipse 3 10 'solid WINDOW2-COLOR))
(define SINGLE-BOOSTER2 . . .)
(define BOOSTER2 (beside SINGLE-BOOSTER2 SINGLE-BOOSTER2))
(define ROCKET-MAIN2 . . .)
(define NACELLE2 . . .)
(define ROCKET-IMG2 . . .)
The values of all these constants remain unchanged from how they are defined in
Chap. 4. In the interest of clarity and brevity some of the code above is omitted as
indicated by the ellipsis.
520 23 Aliens Attack Version 5
The constants to define initial worlds are those needed to construct the initial
world used in the run function or in the testing of the handlers. These constants are
;; Constants for INIT-WORLD
(define AN-IMG-X (/ MAX-CHARS-HORIZONTAL 2))
(define INIT-ROCKET AN-IMG-X)
(define INIT-DIR 'right)
(define INIT-SHOT NO-SHOT)
(define INIT-LOA . . .)
(define INIT-LOS '())
Modular arithmetic (first discussed in Sect. 30) may be used to compute the
image-x and image-y coordinates of each alien as follows:
x = (+ STARTING-X-LINE (remainder n ALIENS-PER-LINE))
y = (quotient n ALIENS-PER-LINE)
Here n is the number of the aliens to be created (e.g., the 12th or the 3rd ). Using
these formulas we define INIT-LOA as follows:
(define INIT-LOA
(build-list NUM-ALIENS
(λ (n)
(make-posn (+ STARTING-X-LINE
(remainder n
ALIENS-PER-LINE))
(quotient n
ALIENS-PER-LINE)))))
The function given to build-list creates a new alien instance using the alien
number it gets as input. Take time to compare the above approach to create an initial
army of aliens with the approach first presented in Sect. 87. You ought to be able to
appreciate how much more concise and elegant the code is now.
* Ex. 297 — Modify the constants to increase the number of aliens and the
number of rows in the initial alien army. Make sure a row of aliens fits and able
to move within confines of the scene.
*** Ex. 298 — Modify the constants to have the alien army in the initial world
displayed in the following formation:
522 23 Aliens Attack Version 5
Given that new characteristics are not being added to Aliens Attack there are no
changes made to the world structure nor to the sample worlds used in testing. They
remain as
;; A world is a structure: (make-world rocket loa dir los)
(define-struct world (rocket aliens dir shots))
Each handler is a function. Therefore, a handler may encapsulate any constants and
auxiliary functions only it uses. In addition, the handler and any of its auxiliary
functions may be refactored to use abstract functions. You will be well-served,
however, if you refactor and test before encapsulating. Always keep in mind that
the process of refactoring may (accidentally) introduce a bug. It is better to catch
bugs before a refactored function is encapsulated. Once encapsulated the refactored
function cannot be directly tested.
One of the goals is to encapsulate within the drawing handler anything that only
it uses. Inspecting the video game code immediately reveals that color constants,
the empty scene, the image-y coordinate of the rocket, and all the image-creating
functions (designed in Chap. 4) along with constants defined using them may be
encapsulated. We note that the image-creating functions do not require refactoring
130 Encapsulating and Refactoring Handlers 523
because they are simple and do not exhibit repetition among them. The definitions
to encapsulate are
(define SHOT-COLOR 'orange)
(define ALIEN-COLOR 'black)
(define WINDOW-COLOR 'darkgray)
(define FUSELAGE-COLOR 'green)
(define NACELLE-COLOR 'red)
(define E-SCENE-COLOR 'pink)
(define E-SCENE
(empty-scene E-SCENE-W E-SCENE-H E-SCENE-COLOR))
(define ROCKET-Y (sub1 MAX-CHARS-VERTICAL))
(define ROCKET-MAIN
(mk-rocket-main-img WINDOW FUSELAGE BOOSTER))
;; image color → image Purpose: Create a rocket nacelle
;; image
(define (mk-nacelle-img a-rocket-main-img a-color) . . .)
(define NACELLE (mk-nacelle-img ROCKET-MAIN NACELLE-COLOR))
Implemented in this manner there is no longer a need for draw-alien and for
draw-shot to be defined. They may be deleted from the game’s program. En-
capsulating the above functions means that draw-ci (refactored using encapsula-
tion in Fig. 99), draw-lox-maker, and draw-lox may also be encapsulated inside
draw-world given that they are not referenced elsewhere.
To complete the refactoring of draw-world add draw-rocket and draw-world
(from Chap. 16) as local functions. The body of the local ought to call the locally
defined draw-world. The tests for (the new global) draw-world remain unchanged.
This handler is the only one that uses process-shooting, move-rckt-left, and
move-rckt-right as auxiliary functions. Therefore, these functions ought to be
encapsulated inside process-key. Observe, however, that move-rckt-left and
move-rckt-right are very similar:
;; rocket → rocket
;; Purpose: Move the given rocket right
(define (move-rckt-right a-rocket)
(if (< a-rocket (sub1 MAX-CHARS-HORIZONTAL))
(add1 a-rocket)
a-rocket))
;; rocket → rocket
;; Purpose: Move the given rocket left
(define (move-rckt-left a-rocket)
(if (> a-rocket 0)
(sub1 a-rocket)
a-rocket))
This suggests using a curried function to eliminate the repetitions and to create
the rocket-moving functions. There are three significant differences: the comparing
predicate, the second value given to this predicate, and the function used to create a
new rocket. Abstracting over the functions yields
(define (make-rocket-mover cmp val f)
(λ (rckt) (if (cmp rckt val) (f rckt) rckt)))
The rocket-moving functions are refactored to
;; rocket → rocket
;; Purpose: Move the given rocket left
(define move-rckt-left (make-rocket-mover > 0 sub1))
;; rocket → rocket
;; Purpose: Move the given rocket right
(define move-rckt-right
(mk-rckt-mvr < (sub1 MAX-CHARS-HORIZONTAL) add1))
526 23 Aliens Attack Version 5
This is the handler with the most auxiliary functions. We shall comment on all the
functions that ought to be encapsulated. We start with the refactored function to
move a list of aliens from Sect. 125:
;; dir → (alien → alien)
;; Purpose: Return a function to move an alien in the
;; given direction
(define (alien-mover-maker a-dir)
(λ (an-alien) (move-alien an-alien a-dir)))
;; image-x>min → image-x
;; Purpose: Move the given image-x>min left
(define (move-left-image-x an-img-x>min)
(sub1 an-img-x>min))
;; image-x<max → image-x
;; Purpose: Move the given image-x<max right
(define (move-right-image-x an-img-x<max)
(add1 an-img-x<max))]
(cond [(eq? a-dir 'right)
(make-posn (move-right-image-x (posn-x an-alien))
(posn-y an-alien))]
[(eq? a-dir 'left)
(make-posn (move-left-image-x (posn-x an-alien))
(posn-y an-alien))]
[else (make-posn
(posn-x an-alien)
(move-down-image-y (posn-y an-alien)))])))]
move-alien))
Observe that move-alien is locally defined, but it is only referenced once and it is
not recursive. This suggests using a λ-expression to represent it instead of locally
defining it. The auxiliary functions to move image coordinates become local to the
new λ-expression as follows:
;; dir → (alien → alien)
;; Purpose: Return a function to move an alien in the
;; given direction
(define (move-alien-maker a-dir)
(lambda (an-alien)
(local [;; image-y<max → image-y
;; Purpose: To move the given image-y<max down
(define (move-down-image-y an-img-y<max)
(add1 an-img-y<max))
;; image-x>min → image-x
;; Purpose: Move the given image-x>min left
(define (move-left-image-x an-img-x>min)
(sub1 an-img-x>min))
;; image-x<max → image-x
;; Purpose: Move the given image-x<max right
(define (move-right-image-x an-img-x<max)
(add1 an-img-x<max))]
(cond [(eq? a-dir 'right)
(make-posn (move-right-image-x (posn-x an-alien))
(posn-y an-alien))]
528 23 Aliens Attack Version 5
** Ex. 299 — The conditional in the above function contains some repetition.
Use abstraction to eliminate repetitions. Is the resulting function easier to un-
derstand?
;; loa → Boolean
;; Purpose: Determine if any alien is at scene’s
;; left edge
(define (any-alien-at-left-edge? a-loa)
(ormap (lambda (an-alien)
(= (posn-x an-alien) MIN-IMG-X))
a-loa))]
(cond [(eq? old-dir 'down)
(if (any-alien-at-left-edge? a-loa) 'right 'left)]
[(eq? old-dir 'left)
(if (any-alien-at-left-edge? a-loa) 'down 'left)]
[else (if (any-alien-at-right-edge? a-loa)
'down
'right)])))
You may be thinking that we can streamline the function by inlining the two local
functions in the body of the cond-expression. Although there is no technical difficulty
in doing so, this is likely to reduce the readability of the code. Therefore, in the interest
of making sure our code clearly communicates how the problem is solved we refrain
from further inlining. You may, of course, disagree and perform the inlining of the
local functions.
** Ex. 300 — Inline any-alien-at-left-edge? and
any-alien-at-right-edge? in the function above. Why is the result more
or less readable?
Section 77.2 discusses the design of remove-hit-aliens and Sect. 77.3 dis-
cusses the design of remove-shots. Both of these auxiliary functions are only used
by process-tick:
;; loa los → loa
;; Purpose: Remove aliens hit by any shot
(define (remove-hit-aliens a-loa a-los)
(cond [(empty? a-loa) '()]
[(hit-by-any-shot? (first a-loa) a-los)
(remove-hit-aliens (rest a-loa) a-los)]
[else (cons (first a-loa)
(remove-hit-aliens (rest a-loa) a-los))]))
Observe that in this test the alien is hit by the first shot and the second shot is
NO-SHOT. This means that all the code is now fully tested. The important lesson to
remember is that encapsulation may remove tests that cover code not covered by the
tests for the function encapsulated into and, thus, requires new tests to be developed.
The tests for this handler are unchanged. This refactoring exercise illustrates two
important lessons. The first is that logic is a useful tool to simplify functions. In this
example, it led to the elimination of any-aliens-alive? and the use of not. The
second is that speculative encapsulation may make us aware of possible simplifica-
tions.
** Ex. 301 — A fellow student proposes that the following game-over? im-
plementation communicates better how the problem is solved:
;; world → Boolean
;; Purpose: Detect if the game is over
(define (game-over? a-world)
(local [(define ANY-REACHED-EARTH
(ormap (lambda (an-alien)
(= (posn-y an-alien) MAX-IMG-Y))
(world-aliens a-world)))
(define ALL-ALIENS-DEFEATED
(empty? (world-aliens a-world)))]
(or ANY-REACHED-EARTH ALL-ALIENS-DEFEATED)))
Is she right? What is better and worse about this implementation? Remember
to consider how an or-expression is evaluated (discussed in Sect. 13.1).
Recall that this handler must determine why game-over? returns #true. Given that
in Aliens Attack 4 it references the same auxiliary functions as game-over? we
can employ the same observations that led to game-over?’s refactoring: logic and
inlining. The refactored function is
;; world → scene throws error
;; Purpose: To draw the game’s final scene
(define (draw-last-world a-world)
(cond [(ormap (lambda (an-alien)
(= (posn-y an-alien) MAX-IMG-Y))
(world-aliens a-world))
(place-image (text "EARTH WAS CONQUERED!" 36 'red)
(/ E-SCENE-W 2)
(/ E-SCENE-H 4)
(draw-world a-world))]
534 23 Aliens Attack Version 5
The run function is the only function that references TICK-RATE. Therefore,
TICK-RATE’s definition ought to be encapsulated. This straightforward refactoring
yields
;; string → world
;; Purpose: To run the game
(define (run a-name)
(local [(define TICK-RATE 1/4)]
(big-bang INIT-WORLD
[on-draw draw-world]
[name a-name]
[on-key process-key]
[on-tick process-tick TICK-RATE]
[stop-when game-over? draw-last-world])))
Recall that INIT-WORLD cannot be encapsulated because it is used to test the
handlers.
The only task remaining is to decide where to place accum and id. These functions
are only referenced by draw-lox, which is encapsulated in draw-world. Therefore,
these functions may also be encapsulated inside draw-world.
This ends the development of Aliens Attack version 5. Take time to appreciate
how much better organized and clearer the game’s code is now. Do you feel that
the code for Aliens Attack version 5 better communicates how the game works than
the code for Aliens Attack version 4? Have you developed a bigger appreciation for
encapsulation, anonymous functions, and abstract functions?
132 What Have We Learned in This Chapter? 535
Abstract functions traverse data of arbitrary size to compute a value. For example,
fact (from Sect. 81) traverses the integers in [0..n] to compute n! and map
traverses a list to compute a value. In essence, they iterate through the elements
contained in an instance of data of arbitrary size. Every time a function that traverses
data of arbitrary size is written a conditional expression is needed. This repetition
suggests an abstraction is needed. In ISL+, this abstraction is provided by for-loops.
A for-loop generates the sequences of values to be iterated over and combines the
values obtained from evaluating an expression (known as its body) to return a value.
They allow programmers to dispense with the coding of conditional expressions. In
addition, they allow programmers to iterate over multiple pieces of data.
The conditionals written to process data of arbitrary size distinguish between
the subtypes of a type and use selector functions to access the components of any
compound subtype. For example, to process a list a conditional expression is used
to distinguish between an empty- and a cons-list. If the given list is a cons-list,
the selector functions first and rest are used to access its components. This is
done every time a list-processing function is written and an abstraction is needed to
avoid the repetition. A similar situation arises when several functions to process a
given defined structure are written. For example, a function to process a posn uses
the selectors posn-x and posn-y. In ISL+, a match-expression provides program-
mers with an abstraction to eliminate this repetitive practice. A match-expression
dispatches on the type of the data and may introduce local variables to capture the
values in a compound piece of data. The use of such expressions can also signif-
icantly improve code readability as done by local-expressions in Sect. 121.2. A
match-expression allows programmers to dispense with writing a local-expression
for this purpose. Determining a data type to introduce local variables or control
program evaluation is called pattern matching.
This chapter explores problem solving and program design using for-loops and
pattern matching. Neither of these increases our computational power. We can com-
pute anything that is possible without them. That is, these are abstractions to make
program development easier and increase readability. In order to use them you must
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 537
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_24
538 24 For-Loops and Pattern Matching
require the 2htdp/abstraction teachpack. That is, the following must appear in
your program before any code:
(require 2htdp/abstraction)
133 For-Loops
For-loops come in two general varieties in ISL+: those that extend the variables
in scope for the body of the for-loop and those that extend the variables in scope
for the body and each subsequent variable introduced by the for-loop. The first we
shall call for-loops and the second we shall call for*-loops. Both varieties require
comprehension clauses that declare local variables and create the values iterated
over.
133.1 for-loops
us that anything implemented using map can also be implemented using a for/list
loop. Consider, for example, the function to move a list of shots from Sect. 130.3:
;; los → los
;; Purpose: To move the given list of shots
(define (move-los a-los)
(map (lambda (a-shot)
(cond [(eq? a-shot NO-SHOT) a-shot]
[(= (posn-y a-shot) MIN-IMG-Y) NO-SHOT]
[else (make-posn
(posn-x a-shot)
(sub1 (posn-y a-shot)))]))
a-los))
This function may be refactored to use a for-loop as follows:
(define (move-los a-los)
(for/list ([a-shot a-los])
(cond [(eq? a-shot NO-SHOT) a-shot]
[(= (posn-y a-shot) MIN-IMG-Y) NO-SHOT]
[else (make-posn
(posn-x a-shot)
(sub1 (posn-y a-shot)))])))
Observe that the body of the for-loop is the same as the body of the λ-expression
in the previous implementation.
Loops in ISL+ may traverse multiple values of arbitrary size at the same time.
At each iteration step the next value in each of the sequences created is used to
evaluate the body of the loop. The loop stops when the end of any sequence is
reached. Whatever remains of the other sequences is ignored. Consider, for example,
the following function from Sect. 111:
;; (listof num) (listof num) → (listof num)
;; Purpose: Return a list with the products of corresponding
;; elements in the given lists
;; Assumption: The given lists are of the same length
(define (mlist L1 L2)
(if (empty? L1)
'()
(cons (* (first L1) (first L2))
(mlist (rest L1) (rest L2)))))
Observe that both lists are simultaneously traversed. To refactor this function using
a for-loop two comprehensions are needed: one for each list. The body of the loop
simply has to multiply the corresponding elements as the values are iterated over.
The resulting function is
(define (mlist L1 L2)
(for/list ([v1 L1] [v2 L2])
(* v1 v2)))
133 For-Loops 541
How do you feel about the result? Do you find it more elegant? Feeling comfortable
with loops is a matter of practice, but you can see that they do offer the opportunity to
dispense with the need to use a conditional expression to distinguish among subtypes
and to use selector functions.
Consider writing a predicate to determine if all the elements of a given list of
numbers are less than a given threshold. There are several options you may choose
from to design this function: use structural recursion on a list of numbers, use
andmap, or use a for/and loop. Perhaps, the first thing that comes to mind is to
iterate through the values of the list. For each value in the list the result obtained
from testing if it is less than the given threshold is anded with the result obtained
from processing the rest of the list. This problem analysis suggests using a for/and
loop.
The following sample instances of a (listof number) are used to develop the
sample expressions using a for/and loop below:
;; Sample instances of lon
(define L0 '())
(define L1 '(89 33 77 56 12 8 7))
(define L2 '(8 31 37 44 12 2 4))
This completes the design of the function. You ought to run the tests and confirm
that they all pass.
** Ex. 302 — Using a for-loop, design and implement a function that takes
as input a list of strings and that returns a list containing their lengths.
*** Ex. 303 — Using a for-loop, design and implement a function to compute
n!.
*** Ex. 304 — Using a for-loop, design and implement a function that adds
the corresponding elements of 4 lists of numbers.
** Ex. 307 — Using a for-loop, design and implement a function that takes
as input a string containing only letters and that returns the string in all lower
case.
*** Ex. 308 — Using a for-loop, design and implement a function that takes
a list of symbols as input and that returns a list of symbols in which every 'a in
the given list is replaced with a 'z.
133.2 for*-loops
The second kind of ISL+ loop, for*-loop, has similar syntax and the same 6 varieties
as for-loops. The syntax only varies by the use of a *:
expr ::= (for*/list (clause+) expr)
::= (for*/and (clause+) expr)
::= (for*/or (clause+) expr)
::= (for*/sum (clause+) expr)
::= (for*/product (clause+) expr)
::= (for*/string (clause+) expr)
The difference with for-loops is the scope of the comprehension variables. While in
a for-loop the scope of a comprehension variable is the loop’s body, in a for*-loop
the scope of a comprehension variable is the remaining comprehension clauses and
the loop’s body. In addition, the sequences of values are iterated over differently. For
example, for every value taken on by the first comprehension variable the second
sequence of values is iterated over and for every value taken on by the second
comprehension variable the third sequence of values is iterated. This pattern is
133 For-Loops 543
a-symbol. The body of the loop simply returns a-symbol to have it consed into
the result.
Abstracting over the sample expressions yields the following function:
;; (listof (listof symbol)) → (listof symbol)
;; Purpose: Flatten the given (listof (listof symbol))
(define (flatten-lolos a-lolos)
(for*/list ([a-los a-lolos] [a-symbol a-los])
a-symbol))
This function is tested as follows:
;; Tests using sample computations for flatten-lolos
(check-expect (flatten-lolos L0) L0-VAL)
(check-expect (flatten-lolos L1) L1-VAL)
*** Ex. 309 — For Aliens Attack a (listof alien) and a (listof shot)
are defined. Using a for*-loop, design and implement a predicate to determine
if any alien has been hit by any shot.
** Ex. 310 — Using a for*-loop, design and implement a function that given
a posn in the first quadrant of the Cartesian plane generates all the posns with
integer coordinates in the sub-plane defined by the origin and the given posn.
546 24 For-Loops and Pattern Matching
To make the semantics of match-expressions clear consider the program in Fig. 106.
The tests are omitted from the figure and are discussed in detail below. To start
consider the following tests:
(check-expect (f X) X-VAL)
(check-expect (f B) B-VAL)
(check-expect (f "Matthew Flatt")
"Matched a string: Matthew Flatt")
The first test passes because the value of X, 42, is given as an argument to f and
is matched in the first stanza of f’s match-expression. The second two tests pass
because they both provide f with a string. The given string does not match the
numeric constant, 42, in the first stanza and is matched in the second stanza because
when given to string? this predicate returns #true.
Now consider what happens for these tests:
(check-expect (f DIMITROVA) DIMITROVA-VAL)
(check-expect (f (make-student "Robby Findler" 'freshman 4))
"Matched a student whose gpa is: 4")
For the arguments provided to f the first two stanzas fail to match because a student
instance is neither the number value 42 nor a string. The third stanza signals a
match because the input is a student instance. This match instantiates three (local)
variables, n, y, and g, for, respectively, the given student’s name, year, and gpa. The
scope for these local variables is the expression in the third stanza. The student’s
gpa, g, is used to build the returned string.
The following tests give f a list that starts with a Boolean:
(check-expect (f BLST) BLST-VAL)
(check-expect
(f '(#false))
"Matched a list that starts with a Boolean: #false")
The first three stanzas in the match-expression do not detect a match. The fourth
stanza, on the other hand, does detect a match because the value of a-value is a
list and the first element of the list is a Boolean. A local variable is instantiated for
the rest of the list, but not for the first list element. The string returned by the fourth
stanza is constructed using the first list element and, therefore, the selector first
must be used to access the needed value.
The fifth stanza detects a match for an arbitrary list and instantiates, B and C, two
local variables, respectively, for the first list element and the rest of the list. The local
variable B shadows the global declaration of B. Consider the following tests:
(check-expect (f NLST) NLST-VAL)
(check-expect
(f '("P. Achten" "J. Hughes" "P. Koopman"))
"Matched a list whose first element is: \"P. Achten\"")
548 24 For-Loops and Pattern Matching
The first three stanzas do not detect a match because a-value is not 42, a string,
or a student in either test. The fourth stanza does not detect a match because the
given lists are of a different type: they do not have a Boolean as the first list element.
For the fifth stanza, the first test instantiates B to 1 (which is not a Boolean) and C to
'(2 3). The local B bound to 1 shadows the B bound to "Basia Mucha". The second
test instantiates B to "P. Achten" (which is not a Boolean) and C to '("J. Hughes"
"P. Koopman"). The local B, once again, shadows the B bound to "Basia Mucha".
The expression in the fifth stanza uses the value of (the local) B to build the returned
list.29
29
The backslashes in the expected value of the second test is DrRacket’s convention to indicate
that a double quote, ", is part of the string.
134 Pattern Matching 549
All the abstraction techniques studied can effectively make programs shorter and
easier to understand. We shall refactor the program developed in Sect. 106 to evaluate
arithmetic expressions. The complete set of functions needed is displayed in Fig. 107.
The first observation is that sum-lon, subt-lon, and mult-lon are all very similar
because they are list-summarizing operations. This suggests refactoring using accum.
The refactoring of sum-lon and mult-lon is
;; lon → number
;; Purpose: To sum the given lon
(define (sum-lon a-lon) (accum 0 id + a-lon))
The bodies of sum-lon and subt-lon have an expression to sum a list of numbers.
To eliminate mostly repeated code we abstract to create a new sum-lon function and
obtain
;; X → X Purpose: Return the given input
(define (id x) x)
Fig. 107 Functions for evaluating arithmetic expressions from Sect. 106
;; <X Y Z> Z (X Y) (Y Z Z) (listof X) Z
;; Purpose: Summarize given list
(define (accum base-val ffirst comb L)
(if (empty? L) base-val
(comb (ffirst (first L)) (accum base-val ffirst comb (rest L)))))
;; X X Purpose: Return the given X
(define (id an-x) an-x)
;; (listof sexpr) (listof number) throws error
;; Purpose: To evaluate the sexprs in the given list
(define (eval-args a-losexpr)
(if (empty? a-losexpr) ()
(cons (eval-sexpr (first a-losexpr)) (eval-args (rest a-losexpr)))))
;; slist number throws error Purpose: To evaluate the given slist
(define (eval-slist sl) (apply-f (first sl) (eval-args (rest sl))))
;; sexpr number throws error Purpose: To evaluate the given sexpr
(define (eval-sexpr a-sexpr)
(cond [(number? a-sexpr) a-sexpr]
[else (eval-slist a-sexpr)]))
;; function (listof number) number throws error
;; Purpose: To apply the given function to the given numbers
(define (apply-f a-function a-lon)
(cond [(eq? a-function +) (sum-lon a-lon)]
[(eq? a-function -) (subt-lon a-lon)]
[else (mult-lon a-lon)]))
;; lon number Purpose: To sum the given lon
(define (sum-lon a-lon)
(if (empty? a-lon) 0 (+ (first a-lon) (sum-lon (rest a-lon)))))
;; lon number throws error Purpose: To subtract the given lon
(define (subt-lon a-lon)
(if (empty? a-lon) (error "No numbers provided to -.")
(- (first a-lon) (sum-lon (rest a-lon)))))
;; lon number Purpose: To multiply the given lon
(define (mult-lon a-lon)
(if (empty? a-lon) 1 (* (first a-lon) (mult-lon (rest a-lon)))))
Consider a function that consumes a list of numbers and then returns a list of the
even numbers in the given list doubled. How can this problem be solved? Reason
about the varieties of (listof number). If the given list is empty, then the result is
the empty list because there are no numbers to process. If the given list is not empty,
then we must distinguish between two types of (listof number): those that start
with an even number and those that start with an odd number. In other words, we are
redefining a list of numbers to be:
;; Data Definition
;; A (listof number) (lon) is either:
;; 1. '()
;; 2. (cons even-number lon)
;; 3. (cons odd-number lon)
552 24 For-Loops and Pattern Matching
If the given list starts with an even number, the double of the first number is consed
to the result of recursively processing the rest of the given list. If the given list starts
with an odd number, the answer is obtained by recursively processing the rest of the
given list.
This problem analysis allows us to develop the following sample lists of numbers
and sample expressions:
;; Sample instances of lon
(define ELON '())
(define LON1 '(0 1 2 3 4 5))
(define LON2 '(7 8 9))
134 Pattern Matching 553
Previous chapters have explored the idea that functions are data. In Chap.20 functions
were passed as input to other functions. In Chap. 22 functions were returned as the
value of function calls. This chapter explores the other side of the coin. Is data
a function? Intuitively, perhaps, the immediate answer that comes to mind is an
unequivocal no. A posn, for example, is not a function given that it does not compute
anything. It stores two values called x and y. You cannot apply a posn to arguments.
The same feels true about lists and trees. If L is a (listof X), what meaning can (L
'first) possibly have? Indeed, it may not be intuitive to think of data as a function.
This lack of intuition, however, is more than anything else a product of training.
After all, has it ever been suggested in a high school Mathematics textbook that a
number is a function? It turns out, however, that data can be a function. That is, data
may be represented using a function. Traditionally, data is thought of separately from
functions. This occurs despite the fact that whenever we create a data definition we
have in mind functions that are valid on the defined data. In other words, we expect
certain behavior from the data. For example, the program for Aliens Attack defines
an alien. It is expected that the alien can be moved right, left, or down, that the
alien may be determined to be at the right or left edge of the scene, and that the alien
may be determined to be hit by a shot. It is not expected that an alien can provide
its name or its distance to the origin on a Cartesian plane. It becomes clear that an
instance of a type is information (i.e., zero or more values) and the functions that are
valid on it.
Indeed, much of this textbook is about defining types: valid values and valid
functions. A posn, for instance, is not simply x and y values. It is also the ability
to extract the x value (i.e., posn-x), to extract the y value (i.e., posn-y), and to
determine if something is a posn (i.e., posn?). Given a posn, the signature and
purpose statement of these selectors, and this predicate, we understand the expected
behavior. Furthermore, we can add new operations and their expected behavior. For
example, we may add a function to compute the distance to the origin. Why, therefore,
should anyone strictly think of a data definition and the operations on the defined type
as two separate entities? Chap. 21 discussed encapsulating related functions. These
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 557
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_25
558 25 Interfaces and Objects
functions can be the operations valid on some data type. If valid type operations are
encapsulated, then there is no technical impediment to also encapsulating the values
that define an instance of a type. To do so we must first learn how to define the
operations valid on a type.
136 Interfaces
A data definition defines a type and in the case of compound data it also defines
the type of the components. It states nothing about the valid type operations. An
interface defines the behavior of a defined type. In other words, an interface specifies
the operations that are valid on a type. Consider the problem of computing the
distance to the origin for a given point on a three-dimensional plane. Following the
steps of the design recipe yields the program displayed in Fig. 109. There are a few
unstated assumptions in the program. First, it is assumed that there is a function
to construct a 3Dposn. Second, it is assumed that there are selector functions for
the components of a 3Dposn. Third, the values of x, y, z, and the selectors are
stored separately from the function dist-origin. Would all this had been clear to
you before reading the preceding chapters? Would you have known that there is a
function 3Dposn-x to extract the x value of an instance of a 3Dposn?
In addition to defining a type, an interface is developed to define the expected
behavior of a type. An interface outlines the valid operations and the returned type.
For a 3Dposn the interface is:
Request x: number
Request y: number
Request z: number
Request distance: number
The interface makes it clear to any reader or user which are the valid operations on a
3Dposn. Observe that an interface says nothing about how a data type and its valid
operations are implemented.
Take a moment to ponder what has just been done. The data definition and the inter-
face explicitly relate x, y, z, 3Dposn-x, 3Dposn-y, 3Dposn-z, and dist-origin.
If they are related, then we ought to be able to encapsulate them into a single pack-
age. Whenever a 3Dposn is constructed, the package returned ought to be able to
perform all the operations in the interface. How can a 3Dposn instance perform
many operations? How does it know what operations to perform?
To achieve this a technique called message-passing is used. An interface is imple-
mented by a constructor function that returns a message-processing function. This is
a curried function that receives as input a message requesting a service. For example,
this function may get the message 'getx requesting the x value of the 3Dposn. An
136 Interfaces 559
;; 3Dposn number
;; Purpose: Return the distance to the origin of the given 3Dposn
(define (dist-origin a-3dposn)
(sqrt (+ (sqr (3Dposn-x a-3dposn))
(sqr (3Dposn-y a-3dposn))
(sqr (3Dposn-z a-3dposn)))))
interface, therefore, must specify the messages used to request a service. We can
now refine the 3Dposn interface to be:
'getx: number
'gety: number
'getz: number
'd2o: number
Observe that there is a unique message (in this case a symbol) associated with each
service. The idea is that the message-processing function determines what value to
compute by examining the message it gets as input. Observe that embedded in the
interface definition is a data definition for a message. A message is an enumeration
type: either 'getx, 'gety, 'getz, or 'd2o.
A constructor function that implements an interface is called a class. A class
encapsulates the values of and the operations on a type. It defines a constructor for
instances of a type. The value returned by a class is called an object. An object is an
instance of an interface and knows how to perform all the services in the interface
using message-passing. The 3Dposn class is displayed in Fig. 110. The class takes as
input 3 numbers and returns a 3Dposn. It is named make-3Dposn to easily identify
its role as a constructor for 3Dposns. Its body is a local-expression that defines an
560 25 Interfaces and Objects
auxiliary function for any value that needs to be computed (in this case only distance
to origin) and the message-processing function called manager. The manager takes
as input a message and returns (the value of) a service defined in the interface.
The body of manager is a cond-expression to distinguish the message varieties. If
a service requires no computation, a value is directly returned. If computation is
required (like computing the distance to the origin), a local function is called. In
this case, manager is a guarded function that throws an error. It is also an object
given that it knows how to compute all the services in the interface and, therefore,
the local-expression returns it.
Testing interfaces requires defining one or more objects and writing tests to check
services are correctly provided. In Fig. 110 two 3Dposn objects are defined. The tests
check the result obtained from passing each message to an object. For example, the
x coordinate of ORIGIN is obtained using (ORIGIN 'getx)—passing the message
'getx to ORIGIN. The expected value is 0. Finally, the tests using sample values use
inlined uses of the constructor to test services and errors.
136 Interfaces 561
Message-passing may reduce the readability of the code. For example, does
(A3DPOSN 'd20) communicate to others that this expression represents A3DPOSN's
distance to the origin? Unless you are intimately familiar with the message-passing
protocol it is likely that this expression is meaningless. Furthermore, it is unlikely
that any programmer (including yourself) will permanently remember the message-
passing protocol in the near and far future. This will make it unnecessarily more
difficult to refine the program.
To mitigate this problem wrapper functions for the services provided by an
interface may be written. A wrapper function hides the details of the implementation.
In this case, it hides the details of message-passing. The idea is to allow programmers
to use 3Dposns without forcing them to know how they are implemented (much like
you do not know how posns are implemented in ISL+). A wrapper function is needed
for each service in the interface. It takes as input an object (and any additional inputs if
any) and its body applies the interface to the appropriate message. Wrapper functions
are designed following the steps of the design recipe.
Figure 111 displays the wrapper functions for 3Dposn. Adding this code to the
one displayed in Fig. 110 allows programmers to use a nicer version of the defined
interface. Instead of explicitly using message-passing, they can use the wrapper
functions. Observe that now programmers have the same interface as the one used
in Fig. 109. Writing wrapper functions does not provide a programmer with new
computational powers, but it is an abstraction that liberates a programmer from the
details of a message-passing protocol.
;; 3Dposn number
;; Purpose: Return the y of the given 3Dposn
(define (3Dposn-y a-3dposn) (a-3dposn gety))
;; 3Dposn number
;; Purpose: Return the z of the given 3Dposn
(define (3Dposn-z a-3dposn) (a-3dposn getz))
;; 3Dposn number
;; Purpose: Return distance to origin of given 3Dposn
(define (dist-origin a-3dposn) (a-3dposn d20))
If a service requires further input, the answer cannot be computed using only the
values stored in an object. That is, the object providing the service (usually referred
to as this) needs information beyond that which it stores. For instance, consider
136 Interfaces 563
adding a service that computes the distance to a given 3Dposn object. In addition to
this object another 3Dposn object is needed. This other object is unknown when
the interface is implemented and, therefore, cannot be provided as input. It is similar
to receiving a message. When the interface is written, there is no way to know which
messages will actually be received as input. The solution for messages is to make
a curried function that consumes the extra input (i.e., a message). The same design
tactic may be employed to add services that require extra input. The interface must
return a function that consumes the extra input.
To illustrate the technique let us add a service to compute the distance of this
3Dposn to a given 3Dposn. The first step is to update the interface as follows:
'getx number
'gety number
'getz number
'd2o number
'd2p 3Dposn → number
The data definition of a message is expanded to include 'd2p for the new distance-
computing service. Given that extra input is needed, the interface returns a function
that consumes the extra input, a 3Dposn, and that returns a number for the distance
between this and the given 3Dposn.
The next step is to refine the manager function to include the new service. This
means adding a stanza to the condition for the new service as follows:
;; message → 3Dposn service throws error
;; Purpose: To manage messages for a 3Dposn
(define (manager m)
(match m
['getx x]
['gety y]
['getz z]
['d2o (dist-origin x y z)]
['d2p distance]
[else (error (string-append "Unknown message to 3Dposn: "
(symbol->string m)))]))
The new stanza matches the new message and returns the distance function (yet
to be written). The distance function must satisfy the return type specified in the
interface definition.
Now, the distance function is designed and implemented. Keep in mind that this
is a local function inside the 3Dposn class. Therefore, this function has in scope all
the variables declared in the class. This is where the power of currying is exploited.
The previous inputs (x, y, and z) are used to compute the distance to the 3Dposn
564 25 Interfaces and Objects
received as input. After following the steps of the design recipe the following local
function is added to the 3Dposn class:
;; 3Dpson arrow number
;; Purpose: Compute the distance from this to the given 3Dposn
(define (distance a-3dposn)
(sqrt (+ (sqr (- x (3Dposn-x a-3dposn)))
(sqr (- y (3Dposn-y a-3dposn)))
(sqr (- z (3Dposn-z a-3dposn))))))
Observe that this function uses the coordinates of this and of the given 3Dposn to
compute the distance.
The final step is to develop a wrapper function for the new distance service. As
before, this is done following the steps of the design recipe. The resulting function is
(define B3DPOSN (make-3Dposn 1 1 1))
* Ex. 316 — Following the steps of the design recipe for interfaces, design and
implement an interface for posn. The services offered by a posn are accessing
the x and y coordinates.
*** Ex. 317 — Following the steps of the design recipe for interfaces, design
and implement an interface for alien as defined for Aliens Attack 5.
**** Ex. 318 — Following the steps of the design recipe for interfaces, design
and implement an interface for car. A car has three characteristics: gas tank
size in liters, maximum speed in kilometers per hour, and kilometers per liter.
566 25 Interfaces and Objects
Designing interfaces for data with variety requires individually reasoning about each
variety. This should not come as a surprise given that individually reasoning about
each variety is how functions to process a union type are designed. Consider the
functions developed in Sect. 72 to compute the length and the sum of a list of quizzes:
;; A quiz grade (qg) is a number in [0..100]
;; loq → number
;; Purpose: To compute the length of the given loq
(define (length-loq a-loq)
(if (empty? a-loq)
0
(+ 1 (length-loq (rest a-loq)))))
;; loq → number
;; Purpose: To compute the sum of the given loq
(define (sum-loq a-loq)
(if (empty? a-loq)
0
(+ (first a-loq) (sum-loq (rest a-loq)))))
Observe that a loq is a union type that has two varieties: empty and non-empty.
For this reason every function that processes a loq has (repeatedly) a conditional
expression just like length-loq and sum-loq. To write the conditional expression
each data variety is individually reasoned about. That is, an answer is formulated for
the empty list and an answer is formulated for the non-empty list.
139 An Abbreviated (listof X) Interface 567
Consider the implications of both varieties of loq offering services. The empty
loq knows how to compute its length and sum. The non-empty loq knows how to
compute its length and sum. This means that a conditional expression is not needed
because a given list, regardless of its variety, knows how to compute its length and
sum. This is called polymorphic dispatch. Polymorphic dispatch, in essence, is
the automatic process of selecting which implementation of an operation to use. It
means that programmers may dispense writing a conditional expression to process a
union type. To achieve this, the code that provides the answer for one variety must be
separated from the code that provides the answer for another variety. For instance,
the code to compute the length of an empty loq must be separated from the code to
compute the length of a non-empty loq. In fact, this must be done for every service
that a loq offers.
How is this separation of code and conditional expression elimination achieved
for a union type? For each subtype there must be a class that encapsulates the code
for that subtype. Observe that each subtype must offer the same services. That is, all
varieties have a common interface. Each class only implements the services for the
subtype it is written for. For instance, the empty loq needs to return 0 for its length
and 0 for its sum. The non-empty loq needs to return 1 plus the length of the rest of
the list for its length and the first quiz plus the sum of the rest of the quizzes for its
sum.
To illustrate interface design for data with variety, an abbreviated version of (listof
X) is implemented. Abbreviated means that this implementation shall not contain all
the functions associated with lists in ISL+. This implementation shall only include
the familiar empty?, first, rest, cons, and map. In addition, it includes the ability
to transform any list implemented using our interface into an ISL+ list.
The steps of the design recipe are outlined. The first three steps guide the rest of
the design. Carefully outlining these steps greatly facilitates the remaining steps.
Two classes are required: one for the empty (listof X) and one for the non-empty
(listof X). The empty (listof X) needs to store no values. The non-empty
(listof X) needs to store two values: an X for the first element of the list and a
(listof X) for the rest of the elements of the list.
The following services are offered by a (listof X):
• Determine if the list is the empty list.
• Access the first element of the list.
• Access the rest of the list.
• Add a new element to the front of this list.
568 25 Interfaces and Objects
• Apply a function to every element of the list and return a list of the results.
• Transform this list into an ISL+ list.
Based on the services outlined in Step 1, 6 return types and 6 message varieties need
to be defined. These may be defined simultaneously in the definition of an interface.
Observe that empty? and the transformation into an ISL+ list return a value with no
need for further input. The services first or rest either return a value or throw an
error with no need for further input. An error is thrown if the first element or rest
of the elements are requested from the empty list. Finally, adding an element to the
front of the list or mapping a function requires further input. This means that these
services must return a function to consume this input.
In order not to confuse ISL+ lists with lists created using the interface devel-
oped here, they are denoted differently. ISL+ lists are denoted as (listof X) and
(listof Y). Lists created using the interface developed are denoted, respectively,
as listofx and listofy.
The interface for a listofx is
;; A listofx is an interface offering
;; 'empty: Boolean
;; 'first: X throws error
;; 'rest: listofx throws error
;; 'cons: X → listofx
;; 'map: (X → Y) → listofy
;; '2Rlst: (listof X)
A message is one of the 6 symbols listed in the interface. Determining if this list
is empty returns a Boolean. Accessing the first element of this list returns a value
of type X or throws an error. Accessing the rest of this list returns a listofx
or throws an error. Adding a value to the front of this list returns a function that
consumes a value of type X and that returns a listofx. Mapping a function onto
this list returns a function that consumes a function of type (X →Y) to map a
single list element and that returns a listofy. Finally, converting this returns an
ISL+ (listof X).
There are a few things known about what needs to be done to implement a class for
listofx. These include the need for a local message-processing function, a local
function to add an element to the front of this, and a function to map a given
function onto this list. In addition, it is known that wrapper functions and tests
139 An Abbreviated (listof X) Interface 569
covering both subtypes are needed for each service. The class template captures all
of these and as a result is quite long. Do not let its length intimidate you. Developing
such a detailed class template makes the next steps of the design recipe much easier.
It is a good time investment to develop a detailed class template. The class template
for listofx is
;; . . . → listofx
;; Purpose: Return a . . . listofx object
(define (class-for-listofx . . .)
(local
..
[ .
;; X → listofx
;; Purpose: Add given X to the front of this list
(define (add2front an-x) . . .)
;; (X → Y) → (listof Y)
;; Purpose: Map the given function to this list
(define (map f) . . .)
;; message → service throws error
;; Purpose: Provide service for the given message
(define (manager m)
(match m
['empty? . . .]
['first . . .]
['rest . . .]
['cons . . .]
['map . . .]
['2Rlst . . .]
[else
(error
(format "Unknown list service requested: ~s" m))]))]
manager))
;; WRAPPER FUNCTIONS
;; listofx → Boolean
;; Purpose: Determine if given listofx is empty
(define (listofx-empty? lox-o) . . .)
;; Sample expressions for listofx-empty?
(define L0E . . .) (define L1E . . .). . .
;; Tests using sample computations for listofx-empty?
(check-expect (listofx-empty? L0) L0E)
(check-expect (listofx-empty? L1) L1E). . .
;; Tests using sample values for listofx-empty?
(check-expect (listofx-empty? . . .) . . .). . .
570 25 Interfaces and Objects
;; listofx → Boolean
;; Purpose: Convert the given listofx to a (listof X)
(define (listofx-2Rlst lox-o) . . .)
;; Sample expressions for listofx-2Rlst
(define L0RL . . .). . .(define L1RL . . .)
;; Tests using sample computations for listofx-2Rlst
(check-expect (listofx-2Rlst L0) L0RL)
(check-expect (listofx-2Rlst L1) L1RL). . .
;; Tests using sample values for listofx-2Rlst
(check-expect (listofx-2Rlst . . .) . . .). . .
Observe that the template definition for the message-processing function, manager,
is specialized for the message data definition developed as part of the interface in
140 The Empty (listof X) Class 571
This section designs the class that implements the interface for the empty listofx.
Steps 4–5 are outlined.
According to the problem analysis done in Step 1 the empty listofx has no char-
acteristics that must be stored. This means that the signature ought to be
;; → listofx
That is, a function with no input that returns a listofx interface. Unfortunately,
ISL+ does not allow functions with 0 parameters. To overcome this the class function
shall have a dummy parameter that is never referenced. Its sole purpose is to allow
for the development of the class function. This makes the signature, purpose, and
class header for the empty listofx:
;; Z → listofx
;; Purpose: Return an empty listofx object
(define (mtList dont-care-param)
Z is the type of the parameter. It may be any type given that it is never used. This
class returns an empty listofx object. The name of the class, mtList, is chosen to
provide any reader of the code an idea of what the class implements.
The message-processing function takes as input a message as defined in Step 2.
The conditional in the function definition template from Step 3 is specialized to
return a value if no further input is required and a function if more input is required
as outlined in the interface developed in Step 2. This function is designed to only
implement the services for the empty listofx. This means that for 'empty? it must
return #true, for 'first and 'rest it must throw an error, for 'cons and 'map it
572 25 Interfaces and Objects
must return a function, and for '2Rlist it must return '(). The function may be
implemented as follows:
;; message → service throws error
;; Purpose: Provide service for the given message
(define (manager m)
(match m
['empty? #true]
['first (error "first requested from the empty list")]
['rest (error "rest requested from the empty list")]
['cons add2front]
['map map]
['2Rlst '()]
[else
(error
(format "Unknown list service requested: ~s" m))]))
Here add2front and map are two auxiliary functions (to be written) that implement
the two services that require further input. This completes Step 4 of the design recipe.
This class, as suggested by the class template, needs at least two auxiliary functions.
The first is the function to implement the cons operation. This function must consume
a value of type X and create a new non-empty listofx. To do so it must use the
constructor for the other variety of listofx. This function must provide as input
the given X value and this list. How do we reference this? Recall that this list
is an object represented by the message-processing function. Therefore, this is
the manager function. Assuming the other class is named consList, this auxiliary
function is
;; X → listofx
;; Purpose: Add given X to the front of this list
(define (add2front an-x) (consList an-x manager))
The mapping function must apply the given function to every element of this
list and return a listofy. There are no elements in this list. Therefore, this list
must only return an empty listofy. The map function for the empty listofx class
is
;; (X → Y) → (listof Y)
;; Purpose: Map the given function to this list
(define (map f) (mtList 'D))
Here 'D is a dummy value to construct an empty listofy. Given that neither of these
functions requires auxiliary functions, this step of the design recipe is completed.
141 The Non-Empty (listof X) Class 573
We now focus on the design of the class for the non-empty listofx. As with the
design of the empty class, steps 4–5 are outlined.
According to the problem analysis done in Step 1 the non-empty listofx has two
characteristics that must be stored: the first element and the rest of the elements. This
means that the signature, purpose, and class header ought to be
;; X listofx → listofx
;; Purpose: Return a nonempty listofx object
(define (consList first rest)
This class returns a non-empty listofx object. The name of the class, consList,
and the name of the parameters, first and rest, are chosen to provide any reader
of the code an idea of what they represent.
The message-processing function takes the same input and serves the same pur-
pose as in the empty listofx class. That is, this function is designed to only
implement the services for the non-empty listofx. This means that for 'empty?
it must return #false, for 'first it must return first, for 'rest it must return
rest, for 'cons it must return add2front, for 'map it must return map, and for
'2Rlist it must return the consing of first and the conversion of rest. The
function may be implemented as follows:
;; message → service throws error
;; Purpose: Provide service for the given message
(define (manager m)
(match m
['empty? #false]
['first first]
['rest rest]
['cons add2front]
['map map]
['2Rlst (cons first (rest '2Rlst))]
[else
(error
(format "Unknown list service requested: ~s" m))]))
As in the empty listofx class, add2front and map are two auxiliary functions
(to be written) that implement the two services that require further input. Pay close
attention to the value returned for '2Rlst. Observe that this, call it L, is converted
with the expression (L '2Rlst). The rest of L is recursively processed using (rest
574 25 Interfaces and Objects
'2Rlst). The only difference is that there is no need to write a conditional expression
thanks to polymorphic dispatch. Remember that rest is a listofx object and it
knows how to convert its value regardless of the list subtype that it is.
This completes Step 4 of the design recipe. Take your time to understand and ap-
preciate that we have structural recursion without a conditional expression. Together
the stanzas for '2Rlst in the empty and non-empty classes are, in essence, the same
code you write as when writing a recursive function. It is simply split among the
classes for each subtype. The code is written differently using message-passing, but
the design is the same.
As the empty listofx class, this class needs two auxiliary functions: add2front
and map. The add2front function must consume a value of type X and create a new
non-empty listofx. To do so it must use the constructor for a non-empty listofx
using the given value and this. Using the constructor for a non-empty listofx
sounds like a recursive call and it is, but remember that the constructor returns a
curried function. There is no recursive traversal here. The recursive call has a single
step that returns an object. Therefore, it is safe to call consList. The function is
implemented the same way as in the empty listofx class:
;; X → listofx
;; Purpose: Add given X to the front of this list
(define (add2front an-x) (consList an-x manager))
If you are thinking that such repetition calls for an abstraction, you are correct. To
abstract away from a class you need to implement an abstract class. An abstract
class contains all the common code among the classes for different subtypes. We
will not delve into abstract classes in this textbook, but you shall learn about them
in an object-oriented programming class.
The mapping function, as expected, must apply the given function to every element
of this list and return a listofy. To do so it must recursively process the rest of
this. The recursive call is implemented using polymorphic dispatch (just as the
converting service). The map function for the non-empty listofx class is
;; (X → Y) → (listof Y)
;; Purpose: Map the given function to this list
(define (map f) (consList (f first) ((rest 'map) f)))
Observe that ((rest 'map) f) is requesting a service from rest. Specifically, it
is requesting its mapping function to provide f to it. Once again, no conditional is
required for this recursive process because rest knows how to map a function to
its elements regardless of the listofx subtype that it is. Given that neither of these
functions requires auxiliary functions, this step of the design recipe is completed.
142 Step 6: Wrapper Functions and Tests 575
This class needs 6 wrapper functions as suggested by the class function template
from Step 3. Start by specializing the sample listofx objects. This may be done as
follows:
;; Sample listofx objects
(define L0 (mtList 'dummyval))
(define L1 (consList
1
(consList
2
(consList
3
(mtList 'dummyval)))))
These are empty and non-empty listofx. These constants are used to write tests
for the wrapper functions.
The wrapper functions are designed using the steps of the design recipe as sug-
gested in the class function template. The wrapper function to determine if a given
listofx is empty requires no further input. This part of the class function template
may be specialized as follows:
;; listofx → Boolean
;; Purpose: Determine if the given listofx is empty
(define (listofx-empty? lox-o) (lox-o 'empty?))
The wrapper function to access the first element of a given listofx requires no
further input. This function, however, may throw an error given that the interface
for this service states that an error may be thrown. This part of the class function
template may be specialized as follows:
;; listofx → X throws error
;; Purpose: Return first element of given list
(define (listofx-first lox-o) (lox-o 'first))
(check-expect
(listofx-2Rlst
(listofx-rest
(consList 'hi
(mtList 'dummyval))))
'())
Once again, observe that the signature specifies that an error may be thrown and
there is only one sample expression. L0 is used to test the error as part of the tests
using sample values.
The wrapper function to add a given value to the front of a given listofx
requires further input. This means that this wrapper function needs to apply the
function returned by the list object to the given value. In addition, recall that function
equivalence is an unsolvable problem (discussed in Sect. 115). This means that the
value of a listofx object cannot be tested for equality with another listofx
object (they are both functions). To overcome this problem, a listofx may be
converted into an ISL+ list for testing. This part of the class function template may
be specialized as follows:
;; listofx X → listofx
;; Purpose: Add the given value to the front of the
;; given listofx
(define (listofx-cons lox-o an-x) ((lox-o 'cons) an-x))
an ISL+ list for testing. This part of the class function template may be specialized
as follows:
;; <X Y> listofx (X → Y) → listofy
;; Purpose: Map given function onto given list
(define (listofx-map lox-o f) ((lox-o 'map) f))
***** Ex. 324 — Design and implement an interface for (btof X) (intro-
duced in Chap. 17).
***** Ex. 325 — Design and implement an interface for natural numbers (in-
troduced in Chap. 14).
You are probably very familiar with distributed programming as a user. In fact, you
probably use distributed programs every day. Such programs include text messaging
systems, multiplayer video games, and social media apps. As a user of a distributed
program you solicit services from another program (usually running on another
computer). For example, you may click on a link in your social media app. The act
of clicking on the link sends a message to a computer operated by the social media
company requesting the contents associated with the link. In turn the social media
company’s computer sends your app a message with the contents of the link. This is
an example of two programs cooperating to solve a problem. Program cooperation
is at the heart of many modern software systems today.
Dividing a problem into several tasks and writing a program for each task that
communicates with the programs for other tasks are called distributed programming.
The tasks cooperate to solve a problem. Each task defines a component that is a
program that solves the task. Each component itself may be divided into subtasks
and may be solved using one or more computers. The components cooperate by
communicating with each other using message-passing. Messages are exchanged
via a network (e.g., the internet). For messages to be exchanged a communication
protocol must be designed. A communication protocol defines the messages that
may be exchanged and when messages are exchanged. For example, a text message
is not sent to your friend until you hit the Enter key or the Send button. Sending a
text message in this manner means you are adhering to a communication protocol:
the message must be a string and the message is only sent after you hit the right
button or key. There may be, of course, a variety in the messages you are allowed to
send (e.g., strings, numbers, etc.). Breaking the communication protocol means that
a message is not sent or is not delivered and communication fails to take place. That
is, there is a breakdown in cooperation.
Messages cannot be arbitrary. That is, there are a finite number of data types that
are suitable for transmission. For example, a number is suitable for transmission
but a posn is unsuitable for transmission. If a component needs to send data that is
unsuitable for transmission, the data must be marshaled. Marshalling is the process
of transforming data that is unsuitable for transmission into data that is suitable
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 583
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_26
584 26 Introduction to Distributed Programming
m
Time
for transmission. To transmit a posn, for example, it must be marshaled into a list
of two numbers for transmission. The component receiving marshaled data must
unmarshal it. Unmarshalling is the process of reconstructing the original data from
marshaled data. For example, a component receiving a marshaled posn as a list of
two numbers must reconstruct the original posn. Marshalling and unmarshalling
functions are inverses of each other. That is, (unmarshal (marshal x)) = x and
(marshal (unmarshal message)) = message. The use of inverse functions is
actually quite common in programming as mentioned in Chap. 2. You may frequently
use encrypt and decrypt to protect sensitive data or compress and uncompress to
reduce file size. Now, you also know about marshalling and unmarshalling data for
transmission.
A pervasively used distributed system architecture is the client–server architec-
ture. A server is a program that provides services or coordinates the cooperation
among clients. A client is a program that performs a task (usually) in cooperation
with other clients to solve a problem. A client on one computer requests services
from the server that typically runs on another computer. All communication between
clients occurs through the server. That is, if Clienti needs to send a message, m,
to Clientj, then Clienti sends m to the server and the server sends m to Clientj.
This communication chain is part of the communication protocol. In a commu-
nication protocol there can be many communication chains for different events. For
example, in a multiplayer Aliens Attack there may be a communication chain that
is started when a player shoots and another that starts when a player moves the
rocket. A communication protocol may be specified using protocol diagrams for
different event-based communication chains. In a protocol diagram the horizontal
axis represents the components and the vertical access represents time (which grows
from top to bottom). Messages are represented by solid arrows from source to des-
tination at a slight angle. The angle is used to emphasize that communication is not
instantaneous. That is, time elapses as messages travel from source to destination.
Dashed arrows are used to represent communication that is implemented by the API
(e.g., a client registering with a server). Figure 112 displays the protocol diagram
144 A Design Recipe for Distributed Programming 585
to send a message, m, from Clienti to Clientj. Time grows from top to bottom.
The first step in the communication chain is Clienti sending m to the server. This
is indicated in the diagram by a solid arrow from Clienti to the Server. Observe
that the arrow’s negative slope clearly indicates that time elapses as m travels from
Clienti to the Server. At the Server, time elapses as m is processed before it
is sent out to Clientj. This is indicated by the gap between the incoming arrow
and the outgoing arrow at the Server. Finally, the outgoing arrow’s positive slope
clearly indicates that time elapses as m travels from the Server to Clientj. When
m arrives at Clientj the communication chain successfully ends.
Servers are generally described by a spectrum that goes from thin to think. A thin
server is one that provides a minimal number of services like message broadcasting.
The server itself does no or very little actual computing. A thick server, on the other
hand, provides services that involve actual computing that directly contributes to
solving a problem. For example, a server may be responsible for computing a value.
As you may have already deduced, distributed programming entails many charac-
teristics that are not present when a program has a single task (e.g., like computing
n!). It is important to carefully design the different components (e.g., clients and
server) and the communication protocol. The magnitude of these tasks may seem
overwhelming at this moment, but practicing the design and implementation of dis-
tributed programs will make you feel more comfortable. As with other topics in
this textbook, there is a design recipe to help guide your development. This design
recipe, however, is less prescriptive than the previously discussed design recipes. It
does not tell you when to use a certain type of expression nor does it dictate what the
parameters to a function must be. Instead, it guides you through the development of
a distributed program assuming that you have mastered the design recipes previously
studied to write functions. Each step still has a specific outcome that gets you closer
to writing a program to solve a problem.
The design recipe for distributed programming is:
1. Divide the problem into components.
2. Draft data definitions for the different components.
3. Design a communication protocol.
4. Design marshalling and unmarshalling functions.
5. Design and implement the components.
6. Test your program.
Step 1 is problem analysis. It asks you to outline how the components cooperate to
solve a problem. This step clearly defines the task (or tasks) carried out by a compo-
nent. For example, the server may be responsible for relaying rocket movements by a
player to all other players in Aliens Attack, while the clients are responsible for draw-
586 26 Introduction to Distributed Programming
ing the world. There are at least two components for every distributed application: a
server and a client.
Step 2 asks you to define the types (or refine the types of an existing program)
required by each component. The types required for each component are not neces-
sarily the same. A server, for example, requires a type definition for messages that
may be sent to it. A client, on the other hand, requires a possibly different type for
the messages it may receive.
Step 3 asks you to develop a communication protocol. This protocol must capture
all the communication chains that may occur. A communication chain is sparked by
an event. For example, in multiplayer Aliens Attack a communication chain is started
when a player shoots. The communication chain may be that the shot created is sent
to the server and the server sends the new shot to all the other players. As part of this
step you must develop data definitions for to-server messages and to-client
messages. These data definitions are used to design the message-processing function
for the server and for the clients.
Step 4 asks you to design marshalling and unmarshalling functions. If the data
that needs to be transmitted in a message is suitable for transmission, there may be
no need to develop these functions for this type of data. If the data to be transmitted
is unsuitable for transmission, then a marshalling and unmarshalling function is
needed to, respectively, create a message suitable for transmission and reconstruct
the original data on the receiving component. Commonly, messages are tagged to
easily distinguish the different varieties. This means that the need for marshalling
and unmarshalling is expected for most applications.
Step 5 asks you to develop the programs for each component. This means that you
need to develop at least two programs: one for the server and one for each client. The
client program may or may not be the same for all clients. Nonetheless, there must
be a separate program (different or copy) for each client. Observe that this means
that a distributed program is written in at least two different files.
Step 6 asks you to run and test your program. As always, if any tests fail you must
redesign.
The development of distributed programs in this textbook uses the API offered by the
universe teachpack. The universe teachpack provides the functionality to develop
distributed multiplayer games. Each player and the server are components. As you
know, a player manages a world and is executed using a big-bang-expression. A
server manages a universe (e.g., a collection of players) and is executed using a
universe-expression. The players in a universe exchange messages with the server.
All communication occurs through the server.
The universe teachpack provides two functions to create messages:
make-package and make-bundle. The first is used by a client to create a structure
that contains a (possibly new) world and a to-server message. The second is used by
145 More on the Universe API 587
the universe server to create a structure that contains a (possibly new) universe, a
list of mails to any of the players, and a list of worlds to be disconnected from the
universe. Observe that a bundle contains an arbitrary number of mails and not an
arbitrary number of to-world messages. A mail is a structure, built using make-mail,
that contains the recipient player and a to-client message.
A message must be an S-expression as defined by the universe API. A
universe S-expression is defined as follows:
A universe S-expression (sexpr) is either a:
1. string
2. symbol
3. number
4. Boolean
5. character
6 (listof sexpr)
Nothing else is suitable for transmission in a universe program. In particular observe
that structures may not be transmitted. If a structure must be transmitted, you must
implement marshalling and unmarshalling functions to do so.
The big-bang syntax required to run a player, as you know, specifies the handlers
that update the game or render the game to the screen. If the player is part of a universe,
it must also register with the server and specify a handler to process messages. To
register with the host a string containing the internet address of the computer running
the server is needed. The string for the internet address of your computer may be
obtained by examining the value of ISL+’s LOCALHOST variable (simply type it at
the prompt in the interactions window). During development and testing you can run
all components on your computer by using LOCALHOST to register players with the
server. For example, the run function for a player may look like this:
;; string → world
;; Purpose: To run the game
(define (run a-name)
(big-bang
INIT-WORLD
(on-draw draw-world)
(on-key process-key)
(on-tick update-world)
(stop-when game-over?)
(register LOCALHOST)
(on-receive process-message)
(name a-name)))
There are two new big-bang clauses here. The register clause tells the universe
teachpack the internet address where the server is running. If this clause contains
LOCALHOST, then the server is running on your computer. If this clause contains a
literal string, like "127.0.0.1", then the server is running at the computer found at
that internet address. It is noteworthy that handlers that may create a new world, like
588 26 Introduction to Distributed Programming
of the universe API that are used in this textbook. If you are interested in further
details about the universe teachpack, you are strongly encouraged to read the
universe documentation found in DrRacket’s Help Desk.
To illustrate the use of the design recipe and of the universe teachpack for dis-
tributed programming the development of a chat tool is presented. The chat tool
developed is not as sophisticated as the chat tools you are probably familiar with, but
it is an interesting first application to develop as you explore distributed program-
ming. The steps of the design recipe are outlined.
The chat tool is designed to allow users to share messages with everyone that is
connected to the server. A user types a string of at most length 20 and sends it to the
group of connected users. The server receives a message from a user and broadcasts
it to the rest of the users. There are, therefore, only two components to design: the
chat client and the chat server. All clients in this application execute the same code.
The client is responsible for drawing the status of the chat displaying the latest four
messages and the message partially typed by the user. The partially typed message
may, of course, be empty. The client is also responsible for processing keystrokes by
the user. If the user hits the Enter key when the new message is not empty, the new
message is added as the last message received by the user and is sent to server for
it to be broadcasted to the other users. If the user hits the Backspace key when the
partially written message is not empty, then its last character is deleted. The Shift
and Tab keys are ignored. Any other key is added to the message as long as the
length of the new message is at most 20.
The server is responsible for adding new users to the universe. A new user is
added only if her name is different from the name of every other user. When a new
user is added, a message is sent to all users informing them of the name of the new
user. The server is also responsible for processing the messages that arrive from the
clients. For each message a new mail is created for every user except the sender of
the message.
The data definitions needed for the server and for the client may differ for some
applications. In other applications they may be the same. Yet in other applications
some data definitions may be shared, while others are not. Our chat application is of
the third variety.
590 26 Introduction to Distributed Programming
To design the client program a data definition for a text message and a world are
needed. We may define a text message, tm, entered by a client as
(define MAX-TM-LEN 20)
;; Sample worlds
(define INIT-WORLD (make-world "" "" "" "" ""))
(define A-WORLD
(make-world "Wanna hang?" "Good thnx" "Good and you?"
"Hi, how are you" "Hi"))
(define B-WORLD (make-world "12345678901234567890" "Guess a number"
"" "" ""))
Here tm1 is the partially written text message and tm5 is the fourth most recent
text message. The template, as expected, outlines all the steps of the design recipe
that must be completed. The sample worlds include the initial world containing all
empty text messages, a world that has no empty text messages, and a world that has
both empty and non-empty text messages and a full partially written message.
146 A Chat Application 591
To design the server program a data definition for a universe is needed. The server
needs to track the worlds in the universe to decide whether a new world may be
connected or to create a list of mails to broadcast a message. Thus, we may define a
universe as follows:
;; A universe is a (listof iworld)
;; ;; Sample universes
(define INIT-UNIV '())
(define A-UNIV (list iworld1 iworld2))
A universe is the list of iworlds connected to the server. The function template to
process a universe is obtained by specializing the template for a (listof X). This
means that in this application the universe is a subtype of (listof X). Therefore,
abstract functions on a (listof X), like map and filter, may be used to process a
universe. There are two sample universes given that there are two varieties. The
sample non-empty universe is built using sample iworlds offered by the API.
592 26 Introduction to Distributed Programming
tm
..
.
tm
;; A to-client message is a tm
146 A Chat Application 593
tm
..
.
tm
This concludes this step of the design recipe. An important lesson to remember is that
a component at the beginning of a solid arrow needs the function, if any, to marshal
the message and the component at the end of a solid arrow needs the function, if any,
to unmarshal the message.
The design of the communication protocol revealed that there is only one type of
to-server message and only one type of to-client message. Given that there
is no variety in these types and the message is suitable for transmission, there is no
need to develop marshalling and unmarshalling functions.
The different components are independently designed using the protocol diagrams
to guide you. The protocol diagrams need to illustrate of the types of messages
that must be sent for different event-driven communication chains leading to data
definitions for to-server and to-client messages.
594 26 Introduction to Distributed Programming
146.5.1 Client
The client, according to our design, must draw the world, process keystrokes, have
a (unique) name, register with the server, and process messages. The run function is
;; string → world
;; Purpose: Run the chat program
(define (run a-name)
(big-bang
INIT-WORLD
(on-draw draw-world)
(on-key process-key)
(name a-name)
(register LOCALHOST)
(on-receive process-message)))
The draw-world function must draw the four most recent tms and the partially
written tm. This can be rendered by drawing the four most recent tms above each
other with the least recent on the top and most recent on the bottom. After this a
red line may be drawn to visually separate them from the partially written tm at the
146 A Chat Application 595
bottom of the scene. No messages need to be sent to the server when the world is
rendered. The function to draw the world is displayed in Fig. 115 and its tests are
displayed in Fig. 116. Functions to create and place tm images are encapsulated.
Observe that there can only be tests using sample values because encapsulated
functions may not be used to write tests.
The manner in which process-key processes keystrokes is exemplified by the
sample expressions in Fig. 117. If the given key is Enter (i.e., "\r") and the partially
written tm is empty, the keystroke is ignored. Otherwise, following the protocol
diagram in Fig. 113, a package is created that sends the partially written tm to the
server and updates the most recently tms. If the given key is Backspace (i.e., “\b”),
the keystroke is ignored if the partially written tm is the empty string. Otherwise,
the last character of the partially written tm is removed. If the given key is either
"Shift" or Tab (i.e., "\t"), the keystroke is ignored. Any other keystroke is added
to the partially written tm if its length is less than or equal to MAX-TM-LEN and
ignored otherwise. The function definition derived from the sample expressions is
displayed in Fig. 118. The fact the function may need to send a message to the
server is reflected in the signature that states the return value may be a world or a
package. The function locally defines variables for the five tms in the given world.
This avoids peppering the conditional expression with expressions using world-
selectors. The tests are displayed in Fig. 117. The tests using sample values illustrate
how the function is expected to behave, while the tests using sample computations
illustrate that the function computes the values in the manner outlined by the sample
expressions.
The remaining handler to design is process-message. The protocol diagrams
in Figs. 113 and 114 inform us that the only message that a client receives is a tm.
The received tm becomes the most recently received message and the other messages
are moved to the next oldest slots. The fourth most recent message is discarded to
596 26 Introduction to Distributed Programming
make room for the new tm. The message-processing handler may be implemented
as follows:
;; world message → world
;; Purpose: Process the given message
(define (process-message a-world a-message)
(make-world (world-tm1 a-world)
a-message
(world-tm2 a-world)
(world-tm3 a-world)
(world-tm4 a-world)))
146 A Chat Application 597
Fig. 119 Sample expressions and tests for adding a world handler
;; Sample expressions for add-new-world
(define ADD-INITU
(make-bundle (list iworld1)
(map
( (iw)
(make-mail iw (string-append (iworld-name iworld3)
" has joined")))
INIT-UNIV)
()))
(define ADD-AUNIV
(make-bundle (list iworld3 iworld1 iworld2)
(map
( (iw)
(make-mail iw (string-append (iworld-name iworld3)
" has joined")))
A-UNIV)
()))
Given that there are no auxiliary functions needed and that all the required
handlers are implemented, the design of the client code is done. Run the tests and
verify that they all pass. At this point do not call the run function because there is no
server for the client to connect to. Once you are satisfied that the tests pass, save two
or more copies of the program in the same directory. Each copy will be a different
component when executed.
146.5.2 Server
According to our design the server must process new clients attempting to register
and must process messages. To add a new iworld to the server the name of the
incoming iworld must be different from the name of any iworld already in the
server. If it is not, the given world is not added and the universe remains unchanged.
This is illustrated by the third sample expression in Fig. 119 that simply is the value of
the given universe. If the name of the arriving iworld is different, then it is added to
the universe. This is illustrated by the first and second sample expressions in Fig. 119.
146 A Chat Application 599
Observe that the outgoing arrows from the server in Fig. 114 are implemented using
map to create a mail for each iworld in the universe. The created mails have a tm
informing the recipient of the new client that has joined the chat. In the first sample
expression the incoming iworld is added to an empty universe. In the second sample
expression the incoming iworld is added to the front of a non-empty universe.
In both cases no worlds are disconnected from the universe and, therefore, the third
argument to make-bundle is '(). The tests illustrate that the function correctly
computes the values of the sample expressions and illustrate the creation of a bundle
using concrete values. Abstracting over the sample expressions yields the function
displayed in Fig. 120.
The incoming arrows to the server in the protocol diagrams in Figs. 113 and 114
inform us that the server only receives messages containing tms. The handler for
processing messages, therefore, only needs to forward an incoming message to all
the worlds except the sender as illustrated in Fig. 113. To achieve this the universe
must be filtered to exclude the iworld that sent the message and mails must be
created for the remaining iworlds. This is illustrated by the sample expressions in
Fig. 121. Locally a list of mails is created using filter and map. The body of the
local-expression creates a bundle with an unchanged universe, the new mails, and
an empty list of iworlds to disconnect. It may seem silly to write a sample expression
using an empty universe (i.e., INIT-UNIV) because there are no iworlds that could
have sent the message. Although this observation is correct, do not forget that the
goal is to validate the function for all universe subtypes. Therefore, a test using the
empty universe must be included. Abstracting over the sample expressions yields
the server’s process-message function.
This completes the design of the server. Run the tests to make sure they all pass.
600 26 Introduction to Distributed Programming
(define IUNIV-MESS
(local [(define new-mails
(map ( (iw) (make-mail iw "Hi!"))
(filter ( (iw) (not (equal? (iworld-name iworld2)
(iworld-name iw))))
INIT-UNIV)))]
(make-bundle INIT-UNIV new-mails ’())))
To run the chat tool on your machine first, run the server and then run one or more
clients. Type messages in each of the clients and see how the messages sent appear
in the other clients. Figure 122 displays a snapshot of a chat session with two clients.
The first client to join is Marco that gets a message when Francisco joins. Both
clients in the snapshot have partially written messages that only they can see because
they have not been sent yet. The window in the middle is the server window. In it you
can see all the server events like clients joining, messages received, and messages
sent.
147 What Have We Learned in This Chapter? 601
Once you are fairly sure that the chat tool is working, you may now use it to
chat with your fellow classmates. Pick a classmate to run the server and get their
internet address. All clients need to substitute LOCALHOST with a string containing
the internet address of the classmate running the server. Once all clients are ready
have the chosen classmate run the server and a client. Once the server is running call
the run function and chat away!
*** Ex. 327 — The chat tool does not display the name of the sender of the
message. Redesign the chat tool to display the name of the sender and the
message all in one line. You may want to increase the maximum length of a tm
for this.
***** Ex. 328 — Redesign the chat tool to allow users to send emojis like a
smiley or a heart. Note that an emoji is an image and images are not suitable for
transmission.
Our next goal is to refine Aliens Attack to allow for multiple players. The game
is used to explore how to design a distributed program using both a thin and a
thick server. First, however, this chapter explores a single-player game refinement
that makes incorporating multiple players easier. The goal is to have all the world-
related data definitions needed for multiple players in place and implemented for a
single-player game. This new implementation of Aliens Attack will serve as the base
code to design a multiple player game. To a player the game will look exactly the
same as before. That is, no new features are added to the game. All that is refined is
the implementation of the existing game.
The refinement is inspired in the expected changes that are needed for a multiplayer
game. Consider the snapshot of multiplayer Aliens Attack displayed in Fig. 123. Take
your time to think about what changes are needed and what elements remain the same
in the world data definition. One difference is that the game has multiple rockets.
The rest of the elements remain the same. That is, there are still multiple aliens, a
direction, and multiple shots. Our initial task, therefore, is clear: define a world
that can have multiple rockets. In this version of the game the number of players
remains at 1 and, therefore, there is no need for a server, a communication protocol,
and all the other necessary features for a distributed program.
The world data definition needs to change from having a rocket to having multiple
rockets that are all allies in repelling the invading alien army. It may be tempting to
simply change the world data definition to have allies instead of a single rocket. We
need, however, to be careful and perform a more detailed problem analysis. Think
about how a player ought to start the game. Should a player start, as in the single-
player game, with a world that has a full army of aliens moving in some hardwired
direction with no shots? This is likely to be fine for the first player that joins the game.
How about the players that join afterwards? These players cannot start with a world
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 603
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_27
604 27 Aliens Attack Version 6
that has a full army of aliens and no shots because the first player may have already
started shooting and neutralizing invaders. This means that the starting world for a
player must be provided by the server. For the first player, it will be what is called
INIT-WORLD in the single-player game. For the other players, it must be the state of
the game when they join.
The above analysis allows us to take a top-down approach to refining the world
data definition. It informs us that there must be variety in the world data definition.
When a player joins the game, her world must be uninitialized and wait for the
server to provide the value of the world. After the server provides this value, the
world becomes a structure. We may now define the world type as follows:
;; A world is either
;; 1. 'uninitialized
;; 2. a structure: (make-world lor loa dir los)
(define-struct world (allies aliens dir shots))
Observe that world is now a union type with two varieties. Furthermore, the second
variety is a structure that contains a lor, instead of rocket, to represent the allies.
The template for a function on a world is:
;; TEMPLATE FOR FUNCTIONS ON A WORLD
;; world . . . → . . .
;; Purpose:
148 Refining the world Data Definition 605
;; (define (f-on-world w . . .)
;; (if (eq? a-world 'uninitialized)
;; ...
;; (. . . (world-allies w). . . (world-aliens w)
;; . . . (world-dir w). . . (world-shots w))))
;;
;; ;; Sample instances of world
;; (define WORLD1 'uninitialized)
;; (define WORLD2 (make-world . . . . . . . . . . . .))
;;
;; ;; Sample expressions for f-on-world
;; (define WORLD1-VAL . . . WORLD1 . . .)
;; (define WORLD2-VAL . . . WORLD2 . . .) . . .
;;
;; ;; Tests using sample computations for f-on-world
;; (check-expect (f-on-world WORLD1 . . .) WORLD1-VAL)
;; (check-expect (f-on-world WORLD2 . . .) WORLD2-VAL) . . .
;;
;; ;; Tests using sample values for f-on-world
;; (check-expect (f-on-world . . . . . .) . . . ) . . .
This new world function template informs us that the refined functions that process
a world must now contain a conditional in the body of the function. In addition, there
must be at least two sample worlds, two sample expressions, and two tests using
sample computations. This is the road map that guides the refinements of the game’s
handlers. It is also noteworthy that the tests using sample values must now construct
worlds based on the new data definition.
The next step is to define lor. Given that the number of allies is not known in
advance, it is data of arbitrary size. The natural choice that comes to mind is a list.
We define lor as follows:
;; An lor is a (listof ally)
The template for a lor is given by specializing the template for a (listof X) with
X = ally.
Can ally be replaced with rocket to complete the data definition refinements?
This requires further problem analysis. In the game each player is expected to control
their own rocket. Assume that an ally is simply a rocket. Consider the player, Pi ,
moving her rocket. Which rocket in Pi ’s lor ought to be moved? If there is only
one rocket in the list, the answer is trivial. What if there are multiple rockets? How
does the program decide which to move? Clearly, Pi wants to move her rocket and
not another player’s rocket. This means that an ally cannot simply be a rocket
because we need to distinguish which player owns each rocket. To do so each ally
606 27 Aliens Attack Version 6
rocket is associated with the name of the world that “owns” it. An ally is, therefore,
defined as follows:
;; An ally is a structure, (make-ally rocket string), with
;; a player’s rocket and name
(define-struct ally (rocket name))
With this data definition, it becomes clear which ally needs to be moved when Pi
moves her rocket: the ally that has Pi ’s name. The template for a function on an ally
is:
;; TEMPLATE FOR FUNCTIONS ON AN ALLY
;; ally . . . → . . .
;; Purpose:
;; (define (f-on-ally an-ally . . .)
;; (. . . (world-allies an-ally) . . . (world-aliens an-ally)
;; . . . (world-dir an-ally) . . . (world-shots an-ally))))
;;
;; ;; Sample instances of ally
;; (define ALLY1 (make-ally . . . . . .))
;;
;; ;; Sample expressions for f-on-ally
;; (define ALLY1-VAL . . . ALLY1 . . .) . . .
;;
;; ;; Tests using sample computations for f-on-ally
;; (check-expect (f-on-ally ALLY1 . . .) ALLY-VAL) . . .
;;
;; ;; Tests using sample values for f-on-ally
;; (check-expect (f-on-ally . . . . . .) . . . ) . . .
Given that there are no further data definitions to develop, we define ally, lor,
and world sample instances as follows:
(define MY-NAME "Yoli Ortega")
The draw-world handler from Aliens Attack version 5 has the following structure:
;; world → scene
;; Purpose: To draw the world in E-SCENE
(define (draw-world a-world)
(local [. . .]
(draw-world a-world)))
608 27 Aliens Attack Version 6
Observe that the assumption is made explicit for the benefit of any reader of the
code. In addition, an auxiliary function to draw the allies is needed. The allies are
drawn in the empty scene just like the rocket is drawn in Aliens Attack version 5.
The design of a local function draw-allies uses draw-lox-maker developed
in Sect. 125. This function may be used to create a function to draw the allies as
follows:
;; lor scene → scene
;; Purpose: Draw the given allies in the given scene
(define draw-allies
(draw-lox-maker (λ (an-ally scn)
(if (string=? (ally-name an-ally) MY-NAME)
(draw-rocket (ally-rocket an-ally) scn)
(draw-ally (ally-rocket an-ally)
scn)))))
The function provided as input to draw-lox-maker draws an ally by distinguishing
between the ally representing the world’s rocket and the rest of the allies. The
world’s rocket is drawn using the existing draw-rocket function. The other allies
must be drawn differently so that the player can easily see her rocket. This may
be accomplished by implementing a new local function, draw-ally, that uses a
different rocket image than that used by draw-rocket as follows:
;; rocket scene → scene
;; Purpose: To draw the rocket in the given scene
(define (draw-ally a-rocket a-scene)
(draw-ci ROCKET-IMG2 a-rocket ROCKET-Y a-scene))
This means that we now have two functions, draw-rocket and draw-ally, that are
almost identical. To eliminate the code repetition we may abstract over the functions
to create a curried function that returns a rocket-drawing function that is specialized
using the rocket image used. This results in the following local functions:
;; image → (rocket scene → scene)
;; Purpose: Create a rocket drawing function
(define (draw-ally-maker rocket-img)
(local [;; rocket scene → scene
;; Purpose: To draw the rocket in the given scene
(define (draw-r a-rocket a-scene)
(draw-ci rocket-img a-rocket ROCKET-Y a-scene))]
draw-r))
)
A test to draw the uninitialized world is added. The test using INIT-WORLD remains
unchanged. The test using a sample value is refactored to draw a world that has an
lor.
The process-key handler from Aliens Attack version 5 has the following structure:
150 The process-key Refinement 611
The assumption that the world is a structure is made explicit. Observe that three
new auxiliary functions are needed: move-ally-right, move-ally-left, and
get-ally.
Let us start with get-ally. This function must extract the ally that has MY-NAME
in it. If this function works, then the signature for process-shooting is satisfied
by the above function. The extraction of the correct ally may be done using filter
as follows:
;; string lor → ally
;; Purpose: Extract ally with given name
;; ASSUMPTIONS: There is a single ally with given name
(define (get-ally a-name a-lor)
(first (filter (λ (an-ally)
(string=? a-name (ally-name an-ally)))
a-lor)))
Observe that it is assumed that there is a single ally with the given name in the given
lor. This means that the list returned by filter contains a single ally and all that
is needed to extract the ally using first. The function given to filter compares
the name given to get-ally with the name of a given ally. Remember that this
function was first designed and tested before being made local to process-key.
The functions to move the player’s rocket are virtually the same. They only
vary by the function used to move the player’s rocket: move-rckt-right or
move-rckt-left. This suggests creating a curried function to create specialized
ally-moving functions. The input to this function is a rocket-moving function and
is implemented as follows:
;; (rocket → rocket) → (string lor → lor)
;; Purpose: Make an ally-moving function
(define (make-ally-mover move-rckt)
(λ (a-name a-lor)
(map (λ (an-ally)
(if (string=? a-name (ally-name an-ally))
(make-ally (move-rckt (ally-rocket an-ally))
(ally-name an-ally))
an-ally))
a-lor)))
The returned function takes as input a name and an lor. The given lor is traversed
using map. The function given to map compares the name given to the returned
function with the name of the ally in the given lor. If they match a new ally is
constructed using the rocket-moving function used to specialize the ally-moving
function. Otherwise, the ally from the given lor is unchanged. The ally-moving
functions are implemented as follows:
;; string lor → lor
;; Purpose: Move ally with given name right
(define move-ally-right (make-ally-mover move-rckt-right))
151 The process-tick Refinement 615
As with the previous handlers, process-tick must be refined using the new world
data definition. The sample expressions may be refined to be:
;; Sample expressions for process-tick
(define AFTER-TICK-WORLD1 (if (eq? INIT-WORLD UNINIT-WORLD)
INIT-WORLD
(process-tick INIT-WORLD)))
(check-expect
(process-tick (make-world
(list (make-ally INIT-ROCKET MY-NAME))
(list (make-posn 2 5))
'left
(list (make-posn 1 6) NO-SHOT)))
(make-world (list (make-ally INIT-ROCKET MY-NAME))
'()
'left
'()))
(check-expect
(process-tick (make-world
(list (make-ally INIT-ROCKET2 MY-NAME))
(list (make-posn
(- MAX-CHARS-HORIZONTAL 2)
10))
'right
(list SHOT2)))
(make-world (list (make-ally INIT-ROCKET2 MY-NAME))
(cons (make-posn MAX-IMG-X 10) '())
'down
(list (make-posn (posn-x SHOT2)
(sub1 (posn-y SHOT2))))))
(check-expect
(process-tick (make-world
(list (make-ally INIT-ROCKET2 MY-NAME))
(list (make-posn MAX-IMG-X 2))
'down
(list (make-posn 15 6))))
(make-world (list (make-ally INIT-ROCKET2 MY-NAME))
(list (make-posn MAX-IMG-X 3))
'left
(list (make-posn 15 5))))
(check-expect
(process-tick (make-world
(list (make-ally INIT-ROCKET2 MY-NAME))
(list (make-posn MIN-IMG-X 2))
'down
(cons (make-posn 2 MIN-IMG-Y) '())))
152 The game-over? Refinement 617
The game-over? predicate must return #false, as before, when the given world’s
list of aliens is empty or when any of the aliens has reached earth. In addition, it
must return #false if the given world is uninitialized. This problem analysis allows
the refinement of the sample expressions to be:
(define GAME-OVER1 (and (not (eq? INIT-WORLD2 UNINIT-WORLD))
(or (ormap
(λ (an-alien)
(= (posn-y an-alien)
MAX-IMG-Y))
(world-aliens INIT-WORLD2))
(empty? (world-aliens INIT-WORLD2)))))
618 27 Aliens Attack Version 6
(check-expect (game-over?
(make-world (list (make-ally 8 MY-NAME))
(list (make-posn 0 MAX-IMG-Y))
'right
(list (make-posn 12 11))))
#true)
(check-expect (game-over?
(make-world (list (make-ally 8 MY-NAME))
(list (make-posn 0 5))
'right
(list (make-posn 0 5))))
#false)
This completes the needed refinements. Make sure that all the tests pass and play
the game. This refinement is the one that is used to create different implementations
of a multiplayer game.
**** Ex. 330 — Change the data definition of the world to include a timer. The
timer is a natural number and is used to control how often the player can shoot.
The player may shoot when the timer is 0. When a player shoots the new world
constructed has a nonzero timer. At every clock tick the timer is decremented,
and the player must wait until the timer reaches 0 to shoot again.
• When a data definition is refined, sample expressions and tests using sample values
for functions that process or produce instances of the refined data definition must
also be refined.
• Data definition refinements may create the need for new data definitions and new
auxiliary functions.
Chapter 28
Aliens Attack Version 7
This chapter refines Aliens Attack 6 into a multiplayer game. Although the design
and implementation of a distributed multiplayer game may seem overwhelming
remember that the design recipe for distributed programming helps you manage the
complexity of its development. The design recipe divides the design into several
smaller and manageable steps. Collectively these steps lead to a distributed program
that is readable to others.
Each section outlines a step of the design recipe. Take time to understand how
the different steps are interrelated. Remember that the steps build on each other.
Therefore, it is important to understand a step before proceeding to the next step.
154 Components
In Aliens Attack 6 a player’s program draws the game, detects when the game is
over, and updates the game by processing key events, clock ticks, and messages from
the server. An intuitive design idea for a multiplayer game is to have each player be a
component that performs the same tasks. In addition, each player component sends
a message to a server when a player induced change occurs. A message is sent to
the server when the player shoots or moves the rocket. A message is received from
the server when it joins the game and when another player shoots, moves it rocket,
arrives, or departs.
The server receives messages from a player, manages the joining of new play-
ers, and manages the departure of players. When a message arrives from a player,
Playeri, the server broadcasts the message to the other players. When a player joins
the game, the server provides it with its starting world and broadcast a message to
all players that they have a new ally. The starting world depends on when Playeri
arrives. If Playeri is the first player to join, the server provides it with the initial
world. If Playeri is not the first player to join, then the server requests the world
from an existing player and sends it to Playeri. In order to properly select recipients
of messages, each player component must have a distinct name. The server rejects
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 621
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_28
622 28 Aliens Attack Version 7
...
Player1 Playern
Draw the world. Draw the world.
Update the world. ... Update the world.
Detect end of the game. Detect end of the game.
Process messages from the server. Process messages from the server.
any new player that has a name already associated with an existing player. When a
player departs the game, the server sends a message to all the other players to remove
the corresponding ally.
Figure 124 visually summarizes the design idea with n players. There are n+1
components: each player and the server. The players and the server exchange mes-
sages to advance the game. Observe that the server is restricted to managing arriving
and departing players and to broadcasting changes made to the game by any player.
It does not perform any task to update the state of the game. That is, this design is
for a thin server. Finally, keep in mind that each player and the server is a separate
file. Therefore, a game with n players has n+1 programs running. Each component
is written in a separate file.
In order to send messages to specific players, the server needs to track the players in
the game. Given that each player is represented as an iworld in our API the universe
is defined as follows:
;; A universe is a (listof iworld), where each iworld
;; has a unique name
shot
ed new
marshal
..
.
shot
new
ha led
mars
The data definitions for the world are defined in Sect. 148. The only detail that
we highlight is that each player must have a unique value for its MY-NAME constant.
This is the value used to give the world a name in the big-bang expression and is
used by the server to distinguish the iworlds it tracks.
The communication chains are sparked by game-changing events that must be com-
municated to one or more players. Think carefully about what events cause a change
in the game that must be communicated to the players. On a player’s side a com-
munication chain is sparked when the player shoots or the player moves her rocket
given that either a new shot or a new ally is created. A communication chain is not
sparked when the clock ticks. Although with every clock tick the state of the game
changes, such a change occurs automatically in all the players. Thus, there is nothing
to communicate. On the server side a communication chain is sparked when a player
joins or leaves the game given that the allies gain or lose an ally.
When a player shoots a new shot is created. This shot does not exist for the other
players. Therefore, the new shot must be communicated to the other players. Figure
125 displays the communication protocol diagram for the communication chain
generated by Playeri shooting. When Playeri shoots, the marshaled new shot is
sent to the server. Upon receiving the message, the server broadcasts it to the rest
of the players. Take note that the new shot is not communicated back to Playeri
given that the new shot already exists in Playeri’s world. This diagram informs
624 28 Aliens Attack Version 7
Fig. 126 Communication protocol diagram for a Playeri moving her rocket
marshaled ro
Time cket moved
et moved
ed rock
marshal
..
.
ved
t mo
d rocke
hale
mars
us that a marshaled new shot message is both a to-server and a to-player message
given that such a message may be an incoming message for both the server and the
players.
When a player moves her rocket, a new ally is created containing the new rocket.
This new ally does not exist for the other players. Therefore, the new ally must be
communicated to the other players. Figure 126 displays the communication protocol
diagram for the communication chain generated by Playeri moving her rocket.
When Playeri moves the rocket, the marshaled new ally is sent to the server.
Upon receiving the message, the server broadcasts it to the rest of the players. This
diagram informs us that a marshaled rocket moved message is both a to-server and
a to-player message.
When a player leaves the game (e.g., they lose their internet connection or close
the game’s window), the other players lose an ally. The server must inform the
remaining players to remove from their world the ally that left the game. Fig. 127
displays the communication protocol diagram for the communication chain generated
by Playeri leaving the game. When Playeri leaves the server gets a message (that is
not generated by our distributed program) indicating that Playeri has disconnected
from the server. The server broadcast a message to remove an ally to all the
remaining players. This diagram informs us that a marshaled remove ally message
is a to-player message.
There are two cases that need to be distinguished when a player tries to join the
game. The first case is when the universe is empty. That is, the player is the first to join
the game. Figure 128 displays the protocol diagram for the sparked communication
chain. The server gets an API-generated message that a new component wants to
join. If the server accepts the new player, it sends a marshaled start world message
156 Communication Protocol 625
Fig. 127 Communication protocol diagram for a Playeri leaving the game
component di
Time sconnect
ally
remove
..
.
lly
ve a
remo
Fig. 128 Communication protocol diagram for Playeri joining an empty universe
new componen
t
Time
art world
marshaled st
Player Server
to the player. This protocol diagram informs us the marshaled start world message
is a to-player message.
The second case is when a player joins a non-empty universe. Figure 129 displays
the communication protocol diagram for the sparked communication chain. The
server gets an API-generated new component message. If the player is admitted, the
server must send out several messages. It sends a marshaled new ally message to
all the players in the universe and a marshaled message to any player (in Fig. 129
it is Player1) requesting the value of the world. Observe that in Fig. 129 all these
messages originate at the same point. This is because all these messages are sent
out when the new player is admitted to the game. A player, upon receiving a world
request message, sends a marshaled world back message to the server. When the
server receives a world back message, it sends a start world message to the player
that just joined the game. This protocol diagram informs us that a marshaled world
back message is a to-server message. It further informs us that a marshaled add ally
message, a marshaled send world message, and a marshaled start world message are
to-player messages.
626 28 Aliens Attack Version 7
Fig. 129 Communication protocol diagram for Playeri joining a non-empty uni-
verse
new componen
t
d ally
led ad
marsha
d
nd worl
ale d se
marsh
marshaled world back
Armed with the knowledge of the message types that need to be exchanged, to-player
message and to-server message data definitions must be developed. According to
our communication protocol, there are six varieties of to-player messages. Think
about what each to-player message must contain. Given that an ally is unsuitable
for transmission, a rocket move message must contain a marshaled ally. Similarly, a
new ally message must contain a marshaled ally. A new shot message must contain
a marshaled shot given that a shot is unsuitable for transmission. A remove ally
message must contain the name of the ally that must be removed. A send world
message must contain the name of the (new) ally that the value of the world is
intended for. Finally, a start message must contain a marshaled world.
contains marshaled allies, marshaled aliens, and marshaled shots. Based on the data
definitions, we may develop function templates and sample instances of marshaled
data as follows:
#| ;; Sample instances of mr
(define MR1 . . .) . . .
;; Sample expressions for f-on-mr
(define MR1-VAL . . .) . . .
;; mr . . . → . . .
;; Purpose:
(define (f-on-mr an-mr . . .)
(local [(define rocket (first an-mr))
(define name (second an-mr))]
. . .))
;; Tests using sample computations for f-on-mr
(check-expect (f-on-mr MR1 . . .) MR1-VAL) . . .
;; Tests using sample values for f-on-mr
(check-expect (f-on-mr . . . . . .) . . .) . . .
;; Sample instances of ma
(define MA1 . . .) . . .
;; Sample expressions for f-on-ma
(define MA1-VAL . . .) . . .
;; ma . . . → . . .
;; Purpose:
(define (f-on-ma an-ma . . .)
(local [(define img-x (first an-ma))
(define img-y (second an-ma))]
. . .))
;; Tests using sample computations for f-on-ma
(check-expect (f-on-ma MA1 . . .) MA1-VAL) . . .
;; Tests using sample values for f-on-ma
(check-expect (f-on-ma . . . . . .) . . .) . . .
;; Sample instances of ms
(define MS1 . . .) . . .
;; Sample expressions for f-on-ms
(define MS1-VAL . . .) . . .
;; ms . . . → . . .
;; Purpose:
(define (f-on-ms an-ms . . .)
(if (list? ms)
(local [(define img-x (first an-ms))
(define img-y (second an-ms))]
. . .)
an-ms . . .))
628 28 Aliens Attack Version 7
;; Sample instances of mw
(define MW1 . . .) . . .
;; Sample expressions for f-on-mw
(define MW1-VAL . . .) . . .
;; mw . . . → . . .
;; Purpose:
(define (f-on-mw an-mw . . .)
(local [(define lomr (first an-ms))
(define loma (second an-ms))
(define dir (third an-ms))
(define loms (fourth an-ms))]
. . .))
;; Tests using sample computations for f-on-mw
(check-expect (f-on-mw MW1 . . .) MW1-VAL) . . .
;; Tests using sample values for f-on-mw
(check-expect (f-on-mw . . . . . .) . . .) . . .
|#
(define MR1 '(10 "iworld1"))
(define MR2 '(8 "iworld3"))
(define MR3 `(11 ,MY-NAME))
(define MALLY1 (list 10 "Rolando"))
(define MALLY2 (list 10 "Margarita"))
(define MA '(14 3))
(define MALIEN1 (list 0 7))
(define MALIEN2 (list 9 4))
(define MS1 NO-SHOT)
(define MS2 '(2 2))
(define MSHOT1 NO-SHOT)
(define MSHOT2 (list 11 9))
(define MLOS '((2 2)))
(define MW '(((7 "iworld1") (3 "iworld2"))
((15 2))
left
((8 5) (7 2))))
(define MWORLD1 (list (list (list 16 "Cristian")
(list 7 "Laura")
(list 6 "Walter"))
(list (list 13 4)
(list 11 11))
'right
156 Communication Protocol 629
(list (list 2 3)
(list 12 8)
(list 5 14))))
(define MWORLD2 (list (list (list 0 "Marce")
(list 8 "Chaty")
(list 4 "Maggie")
(list 2 "Christy"))
'()
'right
'()))
Observe that the templates above are more sophisticated than those developed in the
beginning parts of this textbook. They use a local-expression to define the different
components of a message instead of littering the function body with multiple uses of
list selectors that may carry no meaning for readers trying to understand the design.
The larger than usual sample instances are to facilitate test writing.
Given the data definitions for marshaled data, a to-player message is defined as
follows:
;; A to-player message (tpm) is either
;; 1. (list 'rckt-move ma)
;; 2. (list 'new-shot ms)
;; 3. (list 'new-ally ma)
;; 4. (list 'rm-ally string)
;; 5. (list 'send-world string)
;; 6. (cons 'start mw)
Each tpm has a unique tag to identify it and the appropriate data that is marshaled
if necessary. The template for a function on a tpm and sample tpm instances are
defined as follows:
#| TEMPLATE FOR A FUNCTION on a tpm
;; Sample instances of tpm
(define ST-MSG . . .)
(define MA-MSG . . .)
(define NS-MSG . . .)
(define NA-MSG . . .)
(define RM-MSG . . .)
(define SW-MSG . . .) . . .
;; Sample expressions for f-on-tpm
(define ST-MSG-VAL . . .)
(define MA-MSG-VAL . . .)
(define NS-MSG-VAL . . .)
630 28 Aliens Attack Version 7
(define NA-MSG-VAL . . .)
(define RM-MSG-VAL . . .)
(define SW-MSG-VAL . . .) . . .
;; tpm . . . → . . .
;; Purpose:
(define (f-on-tpm a-tpm . . .)
(local [(define tag (first a-tpm))]
(cond [(eq? tag 'rckt-move)
(local [(define ally (unmarshal-ally
(second a-tpm)))]
. . .)]
[(eq? tag 'new-shot)
(if (list? (second a-tpm))
(local [(define shot (unmarshal-shot
(second a-tpm)))]
(if (eq? shot NO-SHOT)
...
. . .)))]
[(eq? tag 'new-ally)
(local [(define ally (unmarshal-ally
(second a-tpm)))]
. . .)]
[(eq? tag 'rm-ally)
(local [(define name (second a-tpm))]
. . .)]
[(eq? tag 'send-world)
(local [(define name (second a-tpm))]
. . .)]
[(eq? tag 'start)
(local [(define world (unmarshal-world
(rest a-tpm)))
(define allies (world-allies world))
(define aliens (world-aliens world))
(define dir (world-dir world))
(define shots (world-shots world))]
. . .)]
[else
(error (format "Unknown message tag received: ~s"
(first msg)))])))
;; Tests using sample computations for f-on-tpm
(check-expect (f-on-tpm ST-MSG . . .) ST-MSG-VAL)
(check-expect (f-on-tpm MA-MSG . . .) MA-MSG-VAL)
(check-expect (f-on-tpm NS-MSG . . .) NS-MSG-VAL)
(check-expect (f-on-tpm NA-MSG . . .) NA-MSG-VAL)
(check-expect (f-on-tpm RM-MSG . . .) RM-MSG-VAL)
156 Communication Protocol 631
;; Purpose:
(define (f-on-tsm a-tsm . . .)
(local [(define tag (first a-tsm))]
(cond [(eq? tag 'rckt-move) . . .]
[(eq? tag 'new-shot) . . .]
[(eq? tag 'world-back) . . .]
[else
(error
(format "Unknown to-server message type: ~s"
tag))])))
;; Sample expressions for f-on-tsm
(define TSM1-VAL . . .)
(define TSM2-VAL . . .)
(define TSM3-VAL . . .) . . .
;; Tests using sample computations for f-on-tsm
(check-expect (f-on-tsm TSM1 . . .) TSM1-VAL)
(check-expect (f-on-tsm TSM2 . . .) TSM2-VAL)
(check-expect (f-on-tsm TSM3 . . .) TSM3-VAL) . . .
;; Tests using sample values for f-on-tsm
(check-expect (f-on-tsm . . . . . .) . . .) . . .
|#
This template, unlike the template for functions on a tpm, does not suggest unmar-
shalling the components in a given tsm. This is because our design is for a thin
server that is expected to do little more than broadcast the messages it receives. It
has no need to unmarshal the components because it does not process them.
The sample instances for tsms are mostly straight-forward to write because they
contain the same types of marshaled values in them as tpms. We may write sample
values as follows:
;; Sample instances of tsm
(define SRM-MSG RM-MSG)
(define SNS-MSG NS-MSG)
(define SWB-MSG (cons 'world-back (cons "iworld2" MW)))
Marshalling and unmarshalling, respectively, make data unfit for transmission into
data that is fit for transmission and back. In Sect. 156.3.1 four varieties of marshaled
data are defined explicitly: marshaled ally (mr), marshaled alien (ma), marshaled shot
(ms), and marshaled world (mw). Using the generic data definition for (listof X)
a list of mr, a list of ma, and a list of ms are also defined as part of an mw. We shall
develop marshalling and unmarshalling functions for each of these types.
Consider marshalling and unmarshalling an ally. To marshal an ally, you need an
ally as input and you need to output an mr. The mr is constructed by accessing the
157 Marshalling and Unmarshalling 633
;; ally → mr
;; Purpose: Marshal the given ally
(define (marshal-ally an-ally)
(list (ally-rocket an-ally) (ally-name an-ally)))
;; mr → ally
;; Purpose: Unmarshal the given marshaled ally
(define (unmarshal-ally ma)
(local [(define rocket (first ma))
(define name (second ma))]
(make-ally rocket name)))
;; alien → ma
;; Purpose: Marshal the given alien
(define (marshal-alien an-alien)
(list (posn-x an-alien) (posn-y an-alien)))
;; ma → alien
;; Purpose: Unmarshal the given marshaled alien
(define (unmarshal-alien ma)
(local [(define img-x (first ma))
(define img-y (second ma))]
(make-posn img-x img-y)))
;; shot → ms
;; Purpose: Marshal the given shot
(define (marshal-shot a-shot)
(if (eq? a-shot NO-SHOT)
NO-SHOT
(list (posn-x a-shot) (posn-y a-shot))))
;; ms → shot
;; Purpose: Unmarshal the given marshaled shot
(define (unmarshal-shot ms)
(if (list? ms)
(local [(define img-x (first ms))
(define img-y (second ms))]
(make-posn img-x img-y))
ms))
a player in the game may receive a message to send the world. If a player receives
such a message, then its world is initialized. The marshalling function is obtained by
specializing the template for functions on an world as follows:
;; Sample expressions for marshal-world
(define MWORLD3 (list (map marshal-ally
(world-allies INIT-WORLD))
(map marshal-alien
(world-aliens INIT-WORLD))
(world-dir INIT-WORLD)
(map marshal-shot
(world-shots INIT-WORLD))))
;; world → mw
;; Purpose: Marshal the given world
;; ASSUMPTION: The given world is a structure
(define (marshal-world a-world)
(list (map marshal-ally (world-allies a-world))
(map marshal-alien (world-aliens a-world))
(world-dir a-world)
(map marshal-shot (world-shots a-world))))
Observe that the assumption is explicitly stated for any reader of the code, thus,
explaining why the definition lacks a conditional expression to distinguish among
world varieties. In addition, observe that the code, using map, to create the mar-
shaled lists of mr, ma, and ms is inlined instead of creating separate functions. The
unmarshalling function must take as input an mw and return a world. The function
is obtained by specializing the template for functions on an mw as follows:
;; Sample expressions for unmarshal-world
(define UWORLD1 (make-world
(map unmarshal-ally (first MWORLD1))
(map unmarshal-alien (second MWORLD1))
(third MWORLD1)
(map unmarshal-shot (fourth MWORLD1))))
;; mw → world
;; Purpose: Unmarshal the given world
(define (unmarshal-world mw)
(local [(define lomr (first mw))
(define loma (second mw))
(define dir (third mw))
(define loms (fourth mw))]
(make-world (map unmarshal-ally lomr)
(map unmarshal-alien loma)
dir
(map unmarshal-shot loms))))
A function that must send messages to the server is process-key. This means that
some of the player’s outgoing arrows in the protocol diagrams in Figs. 125, 126, 127,
128, and 129 must be implemented by this handler. According to Fig. 125 a new
shot message must be sent when the player shoots. This means that when the player
158 Component Implementation 639
shoots, a new shot is constructed using the player’s rocket coordinate and a new
world is constructed by adding the new shot to the list of shots. These new values
are used to create a package. The message to the server is constructed by making a
list containing the 'new-shot tag and the marshaled new shot. These refinements
are implemented in the third stanza of the conditional for the local process-key
function displayed in Fig. 130.
According to Fig. 126, a rocket move message must be sent when the player moves
her rocket. This means that a package must be constructed with a new world that
has the updated ally for the player and a message that contains the 'rckt-move tag
and the marshaled updated ally. These refinements are implemented in the first two
stanzas of the conditional for the local process-key function displayed in Fig. 130.
This completes the refinements for process-key code. Observe, however, that
the function may return a package or a world. This means that process-key’s
signature must also be refined. In other words, we have done more than just refactor
code. The updated signature is also displayed in Fig. 130. It is also noteworthy that
the only outgoing arrow from a player that still needs to be implemented is the one
for a world back message in Fig. 129. All of the incoming arrows to a player, of
course, still need to be implemented by the function that processes tpms.
640 28 Aliens Attack Version 7
The function to process tpms is designed by specializing the template for functions
on a tpm and the protocol diagrams in Figs. 125, 126, 127, 128, and 129. Develop
a sample expression for each subtype of tpm one at a time using the expressions in
the f-on-tpm definition template. Consider an incoming arrow to a player with an
ally move message in Fig. 126. What needs to be done? For a ally move message,
a local variable is defined to capture the ally embedded in the message and a new
world is constructed by substituting in the list of allies. This is done, for example, as
follows:
(define PM-RMOVE (local [(define ally (unmarshal-ally
(second RM-MSG2)))]
(make-world (replace-ally
ally
(world-allies INIT-WORLD))
(world-aliens INIT-WORLD)
(world-dir INIT-WORLD)
(world-shots INIT-WORLD))))
Note that an auxiliary function is needed to replace an ally in a list of allies.
Consider an incoming arrow to a player with a new shot message in Fig. 125. What
needs to be done? For a new shot message, a local variable is defined to capture the
shot embedded in the message and a new world is constructed by adding the new
shot to the front of the list of shots. This is done, for example, as follows:
(define PM-NSHOT (local [(define shot (unmarshal-shot
(second NS-MSG)))]
(if (eq? shot NO-SHOT)
INIT-WORLD
(make-world
(world-allies INIT-WORLD)
(world-aliens INIT-WORLD)
(world-dir INIT-WORLD)
(cons shot
(world-shots INIT-WORLD))))))
There are two sample expressions because there are two subtypes of shot. Consider
an incoming arrow to a player with a new ally message in Figs. 128 and 129. What
needs to be done? For both cases, a local variable needs to be defined to capture the
ally embedded in the message and a new world is constructed by adding the new
ally to the front of the list of allies. This is done, for example, as follows:
(define PM-NALLY (local [(define ally (unmarshal-ally
(second NA-MSG)))]
(make-world
(cons ally (world-allies INIT-WORLD))
(world-aliens INIT-WORLD)
(world-dir INIT-WORLD)
(world-shots INIT-WORLD))))
Consider an incoming arrow to a player with a remove ally message in Fig. 127.
What needs to be done? For a remove ally message, a local variable is defined to
capture the ally-name embedded in the message and a new world is constructed by
removing the ally from the list of allies. This is done, for example, as follows:
(define PM-RMALLY (local [(define name (second RA-MSG2))]
(make-world (remove-ally
name
(world-allies INIT-WORLD2))
(world-aliens INIT-WORLD2)
(world-dir INIT-WORLD2)
(world-shots INIT-WORLD2))))
Observe that an auxiliary function to remove an ally for a (listof ally) is
needed. Consider an incoming arrow to a player with a send world message in
Fig. 129. What needs to be done? For a send world message, a local variable is
defined to capture the world-name embedded in the message and a package needs to
be constructed because the outgoing arrow with world back message in Fig. 129 must
be implemented. The package is constructed with the world and its marshalling. The
outgoing message must also contain the proper tag and the name of the destination
world. This is done, for example, as follows:
(define PM-SWORLD (local [(define name (second SW-MSG2))]
(make-package
WORLD3
(cons 'world-back
(cons name
(marshal-world WORLD3))))))
Finally, consider an incoming arrow to a player with a start message in Fig. 129.
What needs to be done? For a start message, a local variable is defined to capture
the world embedded in the message. Local variables are also defined to capture
the components of the newly arrived world instance. These components are used to
build a new world by adding the player’s rocket to the list of allies. This is done, for
example, as follows:
642 28 Aliens Attack Version 7
(cons
"Don Marco"
(list (list (list 6 "Doris"))
(list (list 4 9))
"right"
'())))))
;; string → world
;; Purpose: To run the game
(define (run a-name)
(local [(define TICK-RATE 1/4)]
(big-bang
INIT-WORLD
[on-draw draw-world]
[name MY-NAME]
[on-key process-key]
[on-tick process-tick TICK-RATE]
[stop-when game-over? draw-last-world]
[register LOCALHOST]
[on-receive process-message])))
Our initial problem analysis states that the server receives messages from the player,
manages the joining of new players, and manages the departure of players. This means
that three handlers are needed. In addition, the server must broadcast messages to
selected players. This means it must keep track of the players that are in the game.
Based on this, we may define the universe and the run-server function as follows:
;; A universe is a (listof iworld), where each iworld has
;; a unique name
;; Z → universe
;; Purpose: Run the chat server
(define (run-server a-z)
(universe INIT-UNIV
(on-new add-player)
(on-msg process-message)
(on-disconnect rm-player)))
158 Component Implementation 647
The defined universe instances are used to test the handlers and observe that the
second sample universe is built using the sample iworld instances provided by
the API.
Each of the handlers is independently designed using the data definitions and
the communication protocol defined in the previous sections of this chapter. As
you proceed through the design, take note of the importance of understanding the
communication protocol to properly have the components cooperate.
This handler manages the joining of new players. If the joining player’s name is not
already used in the universe, it is allowed to join the game. Otherwise it is rejected.
When a player is allowed to join, as outlined by Figs. 128 and 129, there are two
cases. If it is the first player to join, the protocol diagram in Fig. 128 informs us that
initial world must be sent to it. If it is not the first player to join, the protocol diagram
in Fig. 129 informs us that the existing players must be sent a new ally message and
that the first world in the universe must be sent a send world message.
Develop at least one sample expression for each of the three possibilities. To reject
a player, a bundle may be created that does not change the given universe, that
creates no mails, and that disconnects the player that is attempting to join the game.
A sample expression for a player with a repeated name is:
(define RPT-ADD (make-bundle A-UNIV '() (list iworld1)))
Observe that A-UNIV is unchanged and there are no mails constructed. By not
constructing any mails, the elements of the communication protocol that must still
be implemented remain unchanged. By disconnecting the player from the server,
the player’s game window only displays a message stating that the universe has
disappeared.
To add a player to an empty universe, a bundle is created with a universe that
contains the new player, a start message with the 'start tag and the initial world
marshaled, and an empty list of players to disconnect. A sample expression for this
is:
(define EMP-ADD (make-bundle
(cons iworld1 INIT-UNIV)
(list (make-mail
iworld1
(cons 'start
(marshal-world INIT-WORLD))))
'()))
Observe that the constructed mail implements the arrow from the server to Playeri
in Fig. 128.
To add a player to a non-empty universe, a bundle is created with a universe
that contains the new player, a list of mails containing a send world message to the
first player in the universe and add ally messages to the existing players, and an
648 28 Aliens Attack Version 7
empty list of players to disconnect. The list new ally messages may be constructed
using map to traverse the existing universe. A sample expression for this is:
(define NEW-ADD (make-bundle
(cons iworld3 A-UNIV)
(cons (make-mail
(first A-UNIV)
(list 'send-world
(iworld-name iworld3)))
(map
(λ (iw)
(make-mail
iw
(list 'new-ally
(list
INIT-ROCKET
(iworld-name iworld3)))))
A-UNIV))
'()))
Observe that the list of emails implements the add ally and send world messages
from the server in Fig. 129.
To implement the handler, the template for functions on a universe is specialized
by abstracting over the sample expressions. The result is displayed in Fig. 132. The
158 Component Implementation 649
signature clearly states that a bundle is returned. Each of the three possible cases
has a stanza in the conditional expression. Observe that the initial world, the initial
rocket, and marshal-world must be defined in the server’s program. You may
copy the necessary definitions into the server’s program.
The handler is tested as follows:
;; Tests using sample computations for add-player
(check-expect (add-player A-UNIV iworld1) RPT-ADD)
(check-expect (add-player INIT-UNIV iworld1) EMP-ADD)
(check-expect (add-player A-UNIV iworld3) NEW-ADD)
This handler removes a player that disconnects from the universe. To do so a new
universe is constructed by removing the player and, as indicated by the protocol
diagram in Fig. 127, by sending a remove ally message to the remaining players. The
following sample expression illustrates how this may be done:
(define IW1-RM (local
[(define new-univ
(filter (λ (iw)
(not (string=?
(iworld-name iw)
(iworld-name iworld1))))
A-UNIV))]
(make-bundle
new-univ
650 28 Aliens Attack Version 7
(map (λ (iw)
(make-mail iw
(list 'rm-ally
(iworld-name iworld1))))
new-univ)
'())))
Observe that the list of mails implements the remove ally arrows in Fig. 127 and that
no players are explicitly removed from the server.
Based on the sample expression and Fig. 127, the handler’s implementation is
displayed in Fig. 133. The function returns a bundle because messages must be sent
out. The handler is tested as follows:
;; Tests using sample computations for rm-player
(check-expect (rm-player A-UNIV iworld1) IW1-RM)
The process-message handler must process all incoming tsms. According to the
protocol diagrams in Figs. 125, 126, 127, 128, and 129, the server always sends out
one or more messages when it receives a tsm. This means that this handler must
return a bundle. To process a tsm, we may define local variables for the tag of the
message and for the list of players that a message must be sent to. When the message
is a world back message, the mailing list contains only the recipient player’s iworld.
Otherwise, it contains all the iworlds in the universe other than the player that
sent the message.
Sample expressions are displayed in Fig. 134. Observe that for each the tag and
the mailing list are captured in local variables. For rocket move and a new shot
messages, the server broadcasts them to all the players except the sender as defined
by the protocol diagrams in Figs. 126 and 125. For a world back message, the
marshaled world embedded in the message is sent to the recipient embedded in the
message as a start message to implement the outgoing start message arrow from the
server to Playeri in Fig. 129.
Observe that the local variables are defined the same way for all three tsm sub-
types. Therefore, instead of repeating the same code three times, the local variables
may be defined outside the conditional that distinguishes between the tsm subtypes
as displayed in Fig. 135. Each stanza in the conditional expression creates a bundle
and is obtained from abstracting away the concrete values for the universe, the
sending iworld, and the tsm in the sample expressions. Finally, the handler is tested
as follows:
;; Tests using sample computations for process-message
(check-expect (process-message A-UNIV iworld1 SRM-MSG) PM-RM)
(check-expect (process-message A-UNIV iworld1 SNS-MSG) PM-NS)
(check-expect (process-message A-UNIV iworld2 SWB-MSG) PM-WB)
** Ex. 333 — Improve the tests using sample values for process-message.
* Ex. 335 — Explain what happens if a player with a repeated name is not
disconnected from the server in add-player. Try it out by temporarily changing
the code.
652 28 Aliens Attack Version 7
Before proceeding make sure you have at least two copies of the player’s program
saved (each with a unique MY-NAME value), the players’ tests and the server tests
pass, and you play the game a few times. To play the game as multiple players on
your machine remember to first call run-server in the server file and then run for
each of the player files.
What do you notice, if anything, after playing the game a few times on your
computer? If you run it enough times, you are bound to see that there is a synchro-
nization bug. That is, the state of the game is different for different players. Figure
136 displays two snapshots of the same game at the same time for two different
players. Figure 136a displays the game that Playeri sees and Fig. 136b displays the
game that Playerj sees. Observe that the army of aliens is different. How is this
possible? All the changes made by one player are communicated to all other players.
Therefore, the game state ought to be the same for all players.
654 28 Aliens Attack Version 7
Fig. 136 Snapshot of two different players in the same game at the same time
The problem is due to each player updating its world independently of the other
players and to messages taking time to travel from a player to the server and then to
a receiving player. Let us take a closer look at the consequences of this. Consider
what happens when Playeri shoots. The new shot is added to Playeri’s world
and a new shot message is sent to the server. Playerj has not yet been informed of
the new shot by Playeri. The snapshots in Fig. 137, with new shot only existing
in Playeri’s world, capture the state of the game for each player.
Let us assume that messages are very fast, and they take only one clock tick to
travel from Playeri to the server and then to Playerj. During that clock tick, the
new shot has moved in Playeri’s world, but no movement has taken place by the
copy of the shot received by Playerj. The two worlds are no longer synchronized.
The snapshots in Fig. 138 capture the state of the game for each player.
After the following clock tick, the shot moves in both worlds. In Playeri’s world
the shot hits the alien, but in Playerj’s world the shot has not reached the alien
and, thus, does not hit it. The snapshots in Fig. 139 capture the state of the game for
each player. This means that the alien is removed from Playeri’s world and is not
removed from Playerj’s world. Thus, the army of aliens is different in these two
worlds.
159 A Subtle Bug 655
Does this mean that thin servers are useless? No, it does not mean that. Thin
servers are perfectly fine when clients do not need to be synchronized or when
clients are automatically synchronized by the nature of the application. For example,
thin servers work well for turn-based games, like Tic Tac Toe or chess, where players
take turns to make changes to the game. The players are synchronized after each
change. Now you have a criteria for choosing the kind of server that you ought to
implement for a distributed program.
*** Ex. 336 — Design and implement a two-player Tic Tac Toe game.
***** Ex. 338 — Design and implement a multiplayer Snakes and Ladders
game.
This chapter addresses the design of a multiplayer Aliens Attack using a think server.
In this version of the game only one component, namely the server, is allowed to
make changes to the state of the game. The players, therefore, must be carefully
designed to not make changes to their local copy of the world. Players still need to
have the ability to move their rocket and to shoot. When such actions are taken by a
player, however, the player’s program does not change the world. Instead, a message
is sent to the server and the server creates a new world and sends the new world to
the players. Given that the server is the only component allowed to update the world,
the players cannot become unsynchronized.
The sections of this chapter outline the steps of the design recipe for distributed
computing. As with the material in Chap. 28, take your time to understand how the
steps are interrelated and build on each other.
Figure 140 outlines the components for Aliens Attack 8. The server is responsible
for managing new players that connect to the server. The server does not start the
game until the first player connects. If the arriving player’s world has a name
different from all other worlds connected to the server, then it is allowed to join the
game. Otherwise, it is not allowed to join the game. The server is also responsible for
managing the departure of players. As with Aliens Attack 7, the departing world must
be removed from the universe and the corresponding ally must also be removed. In
addition, the server is responsible for updating the world. This may occur in one of
two ways: a clock tick or a message arrives from a player requesting their rocket
be moved or a shot be created. Whenever the server updates the game, it sends all
the players the new world.
A player component is responsible for drawing the world, processing keystrokes,
detecting the end of the game, and processing messages from the server. The world
is rendered and the end of the game is detected as is done in Aliens Attack 7.
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 657
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_29
658 29 Aliens Attack Version 8
...
Player1 Playern
Draw the world. Draw the world.
Process keystrokes. ... Process keystrokes.
Detect end of the game. Detect end of the game.
Process messages from server. Process messages from server.
Processing keystrokes is different. When the player moves her rocket or shoots, a
message is sent to the server requesting the desired change be made. Receiving a
message from the server means that the server has changed the value of the world
and the world embedded in the message becomes the new world.
The data definitions for the world are the same as those defined in Sect. 148. That
is, a world is either 'uninitialized or is a structure that contains a list of allies
(lor), a list of aliens (loa), a direction, and a list of shots (los). Remember
that each player must have a constant representing a unique name. To facilitate test
development, we define the following world sample instance in addition to those
found in Sect. 148:
(define WORLD4 (make-world (list (make-ally 8 "iworld3")
(make-ally 5 "iworld2"))
(list (make-posn 8 2))
'right
(list (make-posn 8 4))))
In order to send messages to the all players, the server needs to track the players in
the game. As in Aliens Attack 7, this may be done using a list of iworlds. In addition
the server must maintain the state of the game. Therefore, the (current) world must
be part of the universe. Given that the universe is always a finite number of elements
(i.e., two), it may be defined using a structure. The universe, the template for
functions on a universe, and sample instances of universeare as follows:
163 Communication Protocol 659
univ . . . → . . .
Purpose:
(define (f-on-univ a-univ . . .)
(. . .(f-on-loiw (univ-iws a-univ)). . .
. . .(f-on-world (univ-world a-univ). . .)))
marshaled world
..
Time
.
rld ..
led wo .
marsha
orl d
hal ed w
mars
marshaled world
..
Time
.
rld ..
led wo .
marsha
orl d
haled w
mars
Consider what needs to happen when a player attempts to shoot. Recall that the
player may not change the state of the game. Unlike Aliens Attack version 7, a new
shot may not be added to the player’s los and a new shot message must be sent to
the server. Therefore, when a player shoots, a request to add a new shot to the world
is sent to the server. The server processes the request to create a new shot, updates
the world, and sends the new world to all the players. This communication chain
is captured in Fig. 141. Observe that the new world is sent to the player, Playeri,
that requested the new shot be created. In this manner, the shooting player receives
a new world containing the new shot.
Consider what needs to happen when a player moves her rocket. Once again,
a player cannot update the ally that contains her rocket and modify the world.
Instead, a move request is sent to the server. The server updates the world and sends
the new world to all the players. Figure 142 captures this communication chain. As
163 Communication Protocol 661
marshaled world
Time
..
.
orl d
haled w
mars
with shooting, the server sends the new world to the player that requested her rocket
be moved.
Consider the communication chain that must be implemented when a player leaves
the game. The server updates the world. The new world must be sent to all the
players. Figure 143 captures this communication chain. Observe that when Playeri
leaves the game, the new world is sent to all the remaining players.
Consider the communication chain that is sparked when a new player is admitted
to the game. The server must update the world to contain the new ally. This
new world must be transmitted to all the players. This communication chain is
captured in Fig. 144. Observe that, unlike a player joining in Aliens Attack version
7 described using multiple protocol diagrams (Figs. 128 and 129) for two different
communication chains, there is a single protocol diagram for Aliens Attack 8. In
terms of communication, there is no need to distinguish between a player being the
first or not to join the game. Although the server may need to update the world
differently in these two cases, the communication chain is the same. Regardless of
how the server updates the world, only a marshaled world is sent to all the players.
There is no need to request the value of the world from an existing player as depicted
in Fig. 129.
The final server-sparked communication chain starts on a clock tick. The server
updates the world and the new world is sent to all the players. Figure 145 displays
the protocol diagram for a clock tick at the server. Observe that this communication
chain is sparked without any player involvement. That is, it is sparked by a server
event.
662 29 Aliens Attack Version 8
marshaled world
..
Time
.
rld ..
led wo .
marsha
orl d
hal ed w
mars
marshaled world
..
Time
.
rld ..
led wo .
marsha
d
worl
ha led
mars
According to the protocol diagrams in Figs. 141, 142, 143, 144 and 145, there is only
one type of to-player message. Such a message contains a marshaled world. Given
that the world data definition has not been changed, we use the same data definition
for a marshaled world (mw) found in Sect. 156.3.1. We define a to-player message as
follows:
#| ;; A to-player message (tpm) is: (cons 'world mw)
tpm . . . → . . .
Purpose:
(define (f-on-tpm a-tpm . . .)
(. . .(f-on-mw (rest a-tpm) . . .). . .))
;; tsm . . . → . . .
;; Purpose:
(define (f-on-tsm a-tsm . . .)
664 29 Aliens Attack Version 8
Figures 141 142, 143, 144, and 145 inform us that the server needs to marshal a world
and the players need to unmarshal a world. Given that the world data definition is
unchanged from Aliens Attack version 7, the marshal-world, unmarshal-world,
and auxiliary marshalling functions from Sect. 157 may be used in this version of
the game. There is no need to design these functions again.
Observe that tsms only contain data that is suitable for transmission. Therefore,
there is no need to implement marshalling and unmarshalling functions for these
messages.
165 Component Implementation 665
As with Aliens Attack version 7, one of the goals is to reuse as much of the code
as possible from Aliens Attack 6. Based on our problem analysis from Sect. 161, we
can outline how to distribute the handlers from Aliens Attach version 6 among the
player and server components of this version of the game. The player components
need the following, possibly refined, handlers from Aliens Attack version 6:
1. draw-world
2. game-over? (and draw-last-world)
3. process-key
In addition, the player component needs a handler to process tpms. We can, therefore,
define the run function as follows:
;; Z → world
;; Purpose: To run the game
(define (run a-z)
(big-bang
UNINIT-WORLD
[on-draw draw-world]
[on-key process-key]
[stop-when game-over? draw-last-world]
[on-receive process-message]
[name MY-NAME]
[register LOCALHOST]))
Observe that there is not stanza for tick processing. This follows from observing that
it is the server that must update the world when the clock ticks.
The server component needs the following, possibly refined, handlers from Aliens
Attack version 6:
1. process-tick
2. process-key
In addition, the server component needs handlers to process tsms, to add players, and
to remove players. We can, therefore, define the run-server function as follows:
;; Z → univ
;; Purpose: To run the server
(define (run-server a-z)
(local [(define TICK-RATE 1/4)]
(universe
INIT-UNIV
(on-tick process-tick TICK-RATE)
(on-msg process-message)
(on-new add-player)
(on-disconnect rm-player))))
666 29 Aliens Attack Version 8
Observe that process-key is not a handler. This is because key events do not
directly affect the actions of the server. When a tsm is received, the server uses (a
refined version of) process-key to update the game.
This subsection outlines the players’ handlers for Aliens Attack 8. As stated above,
a goal is to reuse existing Aliens Attack version 6 code as possible. We expect
a significant amount of code to be reusable because the world data definition is
unchanged.
Neither of these handlers change the game in Aliens attack version 6. This means that
there is no need for them to be refined to send messages to the server. Furthermore,
given that the world data definition is unchanged, there are no refinements required
at all. The same holds true for the auxiliary function draw-last-world.
In this version of the game, the process-key handler may not update the game.
Therefore, a refinement is needed. Think carefully about what this handler must
do. As in Aliens Attack version 6, if the world in uninitialized then nothing needs
to be done and the existing world is returned. Otherwise, the key pressed must be
processed. If the key pressed is "left" or "right", then according to the protocol
diagram displayed in Fig. 142 a move request must be sent to the server. Similarly, if
the key pressed is " ", then according to the protocol diagram displayed in Fig. 141
a new shot request must be sent to the server. In both cases, a package is constructed
using the current world and the appropriate tsm.
Given that process-key is an encapsulated function, we shall explore its design
by writing tests using sample values. When the player presses the right or left arrow,
a package is created with the given world and a 'move message containing the key
pressed. Tests for the "right" and "left" key events may be written as follows:
;; Tests using sample values for process-key
(check-expect (process-key (make-world
(list (make-ally 10 MY-NAME))
(list (make-posn 7 2))
'right
'())
"right")
165 Component Implementation 667
(make-package (make-world
(list (make-ally 10 MY-NAME))
(list (make-posn 7 2))
'right
'())
(list 'move "right")))
This handler needs to process the single variety of tpm that has a marshaled world
embedded in it. To do so, the embedded marshaled world needs to be unmarshaled.
The following sample expressions illustrate how this is done:
;; Sample expressions for process-message
(define PM-TPM1 (unmarshal-world (rest TPM1)))
(define PM-TPM2 (unmarshal-world (rest TPM2)))
165 Component Implementation 669
Observe that the world required as input by the API and the message’s tag are not
used. Abstracting over the sample expressions yields the following function:
;; world tpm → world
;; Purpose: Update the world with the given tpm
(define (process-message a-world a-tpm)
(unmarshal-world (rest a-tpm)))
The tests using sample computations are written using sample worlds, sam-
ple tpms, and the value of the sample expressions. Tests using sample values are
written using a sample world, and an explicit world message to illustrate that the
unmarshaled world obtained from the embedded marshaled world becomes the
new world value. The tests may be written as follows:
;; Tests using sample computations for process-message
(check-expect (process-message INIT-WORLD TPM1) PM-TPM1)
(check-expect (process-message INIT-WORLD2 TPM2) PM-TPM2)
This subsection outlines the server’s handlers for Aliens Attack 8. As with the player
component, we shall reuse as much code as possible from Aliens Attack version 6.
This reuse mostly refers to the tick handler and the auxiliary function for the message
handler to process player requests.
670 29 Aliens Attack Version 8
According to the API the tick handler must take as input a universe and return
a universe. This signature is different from the process-tick function from
Aliens Attack version 6 and, therefore, a new universe-processing function is needed.
Think carefully how a universe ought to be processed. If the given universe is
INIT-UNIV, it means that a player has not joined and the universe ought to remain
unchanged. Otherwise, the universe ought to be updated. The list of iworlds remains
unchanged. The game, on the other hand, needs to be updated. This may be achieved
by calling process-tick from Aliens Attack version 6 with the universe’s game
value. This means that the process-tick and all its local auxiliary functions from
Aliens Attack version 6 need to be local to the server’s process-tick. In addition,
as indicated by the protocol diagram in Fig. 145, the new world value must be sent
to all the players.
The handler is outlined as follows:
;; univ → bundle
;; Purpose: Create a new universe after a clock tick
(define (process-tick a-univ)
.
(local [..
;; world → world
;; Purpose: Create new world after a clock tick
;; ASSUMPTION: The world is a structure
(define (process-tick a-world) . . . )]
(if (equal? a-univ INIT-UNIV)
(make-bundle a-univ '() '())
(local [(define new-game
(process-tick
(univ-game a-univ)))]
(make-bundle
(make-univ (univ-iws a-univ) new-game)
(map (λ (iw)
(make-mail
iw
(cons 'world
(marshal-world new-game))))
(univ-iws a-univ))
'())))))
In this design the function always returns a bundle even when no messages are
sent to the players. The local-expression encapsulates process-tick and all its
auxiliary functions from Aliens Attack version 6. The body of the local-expression
distinguishes between a universe that has and that does not have at least one player.
Observe that in both cases the list of iworlds is unchanged and no iworlds are
disconnected from the universe. No mails are constructed when the game has not
165 Component Implementation 671
started. When the game has already started, all players get the new world value that
is locally defined (i.e., new-game).
The handler is tested in a similar fashion to testing done in previous versions of
the game. That is, the tests illustrate that changes to the aliens, the direction,
and the shots for the given universe’s world instance are correctly made and that
the list of iworlds remains unchanged. Tests may be written as follows:
;; Tests using sample values for process-tick
(check-expect
(process-tick
(make-univ (list iworld1 iworld3)
(make-world (list (make-ally 9 "iworld1")
(make-ally 2 "iworld3"))
(list (make-posn 2 5))
'left
(list (make-posn 3 6) NO-SHOT))))
(make-bundle
(make-univ (list iworld1 iworld3)
(make-world (list (make-ally 9 "iworld1")
(make-ally 2 "iworld3"))
(list (make-posn 1 5))
'left
(list (make-posn 3 5))))
(list (make-mail iworld1
(list 'world
(list (list 9 "iworld1")
(list 2 "iworld3"))
(list (list 1 5))
'left
(list (list 3 5))))
(make-mail iworld3
(list 'world
(list (list 9 "iworld1")
(list 2 "iworld3"))
(list (list 1 5))
'left
(list (list 3 5)))))
'()))
(check-expect
(process-tick
(make-univ (list iworld3)
(make-world (list (make-ally 6 "iworld3"))
(list (make-posn
(- MAX-CHARS-HORIZONTAL 2)
672 29 Aliens Attack Version 8
10))
'right
(list SHOT2))))
(make-bundle
(make-univ
(list iworld3)
(make-world (list (make-ally 6 "iworld3"))
(list (make-posn
(sub1 MAX-CHARS-HORIZONTAL)
10))
'down
(list (make-posn (posn-x SHOT2)
(sub1 (posn-y SHOT2))))))
(list (make-mail
iworld3
(list 'world
(list (list 6 "iworld3"))
(list (list (sub1 MAX-CHARS-HORIZONTAL) 10))
'down
(list (list (posn-x SHOT2)
(sub1 (posn-y SHOT2)))))))
'()))
(check-expect (process-tick
(make-univ
(list iworld2)
(make-world (list (make-ally 14 "iworld2"))
(list (make-posn MAX-IMG-X 2))
'down
(list (make-posn 15 6)))))
(make-bundle
(make-univ
(list iworld2)
(make-world
(list (make-ally 14 "iworld2"))
(list (make-posn MAX-IMG-X 3))
'left
(list (make-posn 15 5))))
(list (make-mail
iworld2
(list 'world
(list (list 14 "iworld2"))
(list (list MAX-IMG-X 3))
'left
(list (list 15 5)))))
'()))
165 Component Implementation 673
(check-expect (process-tick
(make-univ
(list iworld1)
(make-world (list (make-ally 3 "iworld1"))
(list (make-posn MIN-IMG-X 2))
'down
(list (make-posn 2 MIN-IMG-Y)))))
(make-bundle
(make-univ
(list iworld1)
(make-world
(list (make-ally 3 "iworld1"))
(list (make-posn MIN-IMG-X 3))
'right
'()))
(list (make-mail
iworld1
(list
'world
(list (list 3 "iworld1"))
(list (list MIN-IMG-X 3))
'right
'())))
'()))
This handler is written by specializing the template for functions on a tsm. If the
given tsm’s tag is shoot or move, then the universe’s game needs to be updated.
In both cases this may be accomplished by refining process-key from Aliens
Attack version 6. Think carefully about why a refinement is needed. The code for
process-key always moves the ally or creates a shot for a single player. In this
version of the game the server needs to do so for an arbitrary player. If process-key
is made local to process-message,then the name of the iworld making the request
and the universe’s world value are in scope and may be used to correctly move or
shoot. This means that process-key only needs the key to process as input. The
key is used as expected to move an ally or to create a shot for an ally. The value
returned by process-key, therefore, must be a bundle that has a univ with the new
world value and the mails containing this new value for the players.
Figure 146 outlines the server’s process-message handler. The signature clearly
states that the handler may throw an error. This occurs when a message with an unrec-
ognized tag is received. If the message’s tag is 'shoot or 'move, then process-key
674 29 Aliens Attack Version 8
is called with the key that needs to be processed. When the tag is 'move, the needed
key is extracted from the given tsm. The function process-key locally defines all
.
the functions that were locally defined for Aliens Attack 6 (indicated by the ..). In ad-
dition, it locally defines nw for the new world created. This new world is created by
processing the given key. The list of allies in this new world is computed by determin-
ing the given key’s variety. Observe that move-ally-right and move-ally-left
are called with name instead of MY-NAME as done in Aliens Attack version 6. This
is how moving an arbitrary ally is achieved. The aliens and direction in the
new world remain unchanged. The list of shots for the new world is obtained by
determining if the given key is " ". If not, the shots remain unchanged. If so, a
new shot is added to the game. Observe that get-ally is called with name instead
of MY-NAME as done in Aliens Attack version 6. This is how creating a shot for
an arbitrary ally is achieved. Finally, the body of the local-expression returns a
165 Component Implementation 675
bundle. The new universe is constructed using the existing list of iworlds and
the new world. The list of mails implements all the arrows from the server to all
the players in Figs. 141 and 142. No iworlds are disconnected from the server when
a player’s request is processed.
Testing the handler requires at least one test for the error thrown and one test for
each tsm subtype. Given that functions are encapsulated, the only viable testing is
using sample values. Testing the error generated is done by providing a tsm with an
invalid tag:
(check-error
(process-message OTHR-UNIV iworld2 (list 'move-left 'left))
(format "Unknown to-server message type: ~s"
(list 'move-left 'left)))
Testing the processing of a move message is done with two different tsms: one for
each direction. The tests are written using a sample universe and sample tsms.
The expected bundles illustrate that the list of iworlds remains unchanged and that
the proper ally is moved in the proper direction. Sample tests are as follows:
(check-expect (process-message OTHR-UNIV iworld2 MV-LEFT)
(make-bundle
(make-univ (list iworld1 iworld2)
(make-world
(list (make-ally 7 "iworld1")
(make-ally 8 "iworld2"))
(list (make-posn 3 3))
'right
(list (make-posn 1 2))))
(list
(make-mail iworld1
(list 'world
(list (list 7 "iworld1")
(list 8 "iworld2"))
(list (list 3 3))
'right
(list (list 1 2))))
(make-mail iworld2
(list 'world
(list (list 7 "iworld1")
(list 8 "iworld2"))
(list (list 3 3))
'right
(list (list 1 2)))))
'()))
676 29 Aliens Attack Version 8
'right
(list (list 7 14) (list 1 2))))
(make-mail
iworld2
(list
'world
(list (list 7 "iworld1") (list 9 "iworld2"))
(list (list 3 3))
'right
(list (list 7 14) (list 1 2)))))
'()))
The handler to add new players must reject iworlds that have a name that is already
in use in the universe’s list of iworlds. In such a case, the handler returns a
bundle with an unchanged universe, an empty list of mails, and a one-iworld
list to disconnect from the server that contains the iworld attempting to connect.
The following sample expression illustrates the design:
;; Sample expressions for add-player
(define RPT-ADD (make-bundle OTHR-UNIV '() (list iworld1)))
OTHR-UNIV already has an iworld with the name "iworld1". The world trying to
connect is rejected. If the iworld connecting does not have a name already in use,
two cases must be distinguished: this is the first player that joins or it is not. If it is the
first player to join, the initial world containing the ally that just joined is used to
create a new universe. Otherwise, the new ally is added to the existing game. In
both cases the new iworld is added to the universe’s list of iworlds. According
to the protocol diagram in Fig. 144, a world message must be sent to all players.
The new world value containing the new ally must be marshaled to send to all the
players. The following sample expressions illustrate how this may be achieved:
(define
EMP-ADD
(local
[(define new-iws (cons iworld2 (univ-iws INIT-UNIV)))
(define game (univ-game INIT-UNIV))
(define new-game
(if (equal? game UNINIT-WORLD)
(make-world (list (make-ally
INIT-ROCKET
(iworld-name iworld2)))
INIT-LOA
INIT-DIR
INIT-LOS)
678 29 Aliens Attack Version 8
(define
NEW-ADD
(local
[(define new-iws (cons iworld3 (univ-iws OTHR-UNIV)))
(define game (univ-game OTHR-UNIV))
(define new-game (if (equal? game UNINIT-WORLD)
(make-world
(list (make-ally
INIT-ROCKET
(iworld-name iworld3)))
INIT-LOA
INIT-DIR
INIT-LOS)
(make-world
(cons (make-ally
INIT-ROCKET
(iworld-name iworld3))
(world-allies game))
(world-aliens game)
(world-dir game)
(world-shots game))))]
(make-bundle (make-univ new-iws new-game)
(map (λ (iw)
(make-mail
iw
(cons 'world
(marshal-world new-game))))
new-iws)
'())))
165 Component Implementation 679
Local variables are defined for the new list of iworlds and for the new world value.
The new list of iworlds is obtained by adding the joining iworld to the list of
iworlds in the given universe. An if-expression is used to define the new world
value by distinguishing whether or not the given world is the first to join. The body of
the local-expression returns a bundle with the new universe and a list of mails
with the marshaled new world.
Based on the sample expressions, the add-player handler is defined as follows:
;; universe iworld → bundle
;; Purpose: Add new world to the universe
(define (add-player a-univ an-iw)
(if (member?
(iworld-name an-iw)
(map iworld-name (univ-iws a-univ)))
(make-bundle a-univ '() (list an-iw))
(local [(define new-iws (cons an-iw (univ-iws a-univ)))
(define game (univ-game a-univ))
(define new-game (if (equal? game UNINIT-WORLD)
(make-world
(list (make-ally
INIT-ROCKET
(iworld-name an-iw)))
INIT-LOA
INIT-DIR
INIT-LOS)
(make-world
(cons (make-ally
INIT-ROCKET
(iworld-name an-iw))
(world-allies game))
(world-aliens game)
(world-dir game)
(world-shots game))))]
(make-bundle
(make-univ new-iws new-game)
(map
(λ (iw)
(make-mail
iw
(cons 'world (marshal-world new-game))))
new-iws)
'()))))
The handler is tested as follows:
;; Tests using sample computations for add-player
(check-expect (add-player OTHR-UNIV iworld1) RPT-ADD)
680 29 Aliens Attack Version 8
The handler to remove a player needs to create a new world value by eliminating
the ally from the disconnected iworld and by removing the disconnected iworld
from the list of iworlds. This may be accomplished by filtering the list of allies and
the list of iworlds. In addition, as specified by the protocol diagram in Fig. 143, the
new world value must be sent to all the (remaining) players. The following sample
expressions illustrate how this is accomplished:
;; Sample expressions for rm-player
(define RM-IW1 (local
[(define iws (univ-iws OTHR-UNIV))
(define game (univ-game OTHR-UNIV))
(define new-iws (filter
(λ (iw)
(not (string=?
(iworld-name iworld1)
(iworld-name iw))))
iws))
(define new-game (make-world
(filter
(λ (a)
(not (string=?
(iworld-name iworld1)
(ally-name a))))
(world-allies game))
(world-aliens game)
(world-dir game)
(world-shots game)))]
(make-bundle (make-univ new-iws new-game)
(map (λ (iw)
(make-mail
iw
(cons 'world
(marshal-world
new-game))))
new-iws)
'())))
(iworld-name iw))))
iws))
(define new-game
(make-world
(filter
(λ (a)
(not (string=?
(iworld-name iworld2)
(ally-name a))))
(world-allies game))
(world-aliens game)
(world-dir game)
(world-shots game)))]
(make-bundle (make-univ new-iws new-game)
(map (λ (iw)
(make-mail
iw
(cons 'world
(marshal-world
new-game))))
new-iws)
'())))
Observe that filter is used to remove the disconnected iworld and its corre-
sponding ally. The creation of the tpms is done using map and the new list of
iworlds.
Abstracting over the sample expressions yields the function for this handler:
;; univ iworld → bundle
;; Purpose: Remove given iw from universe and game
;; ASSUMPTION: Given univ is not INIT-UNIV
(define (rm-player a-univ an-iw)
(local [(define iws (univ-iws a-univ))
(define game (univ-game a-univ))
(define new-iws (filter
(λ (iw)
(not (string=? (iworld-name an-iw)
(iworld-name iw))))
iws))
(define new-game (make-world
(filter
(λ (a)
(not (string=? (iworld-name an-iw)
(ally-name a))))
(world-allies game))
(world-aliens game)
(world-dir game)
(world-shots game)))]
166 A Subtle Problem 683
Play the game with one or two friends. Assuming your internet connection is fast
enough the game ought to run smoothly. However, as the number of players increases,
the game may become choppy. That is, the game becomes sluggish or momentarily
stops. This may be due to communication overhead. Communication overhead is
the proportion of time that is spent exchanging messages instead of advancing the
684 29 Aliens Attack Version 8
state of the game. If communication overhead is large enough, the game becomes
slow.
In distributed programming communication is necessary and, therefore, some
degree of communication overhead is necessary. It is desirable, however, to keep
this overhead small to make programs faster. There is no universal solution to this
problem. That is, implementing a solution may or may not produce the desired
result. Common approaches to reduce communication overhead are to limit the
number messages exchanged and reduce the size of the messages exchanged. In
Aliens Attack 8, for example, the world value is always sent to the players. This is
done despite the fact that only one component of the world has changed. A different
communication protocol may transmit to the players only the components that have
changed. This may or may not be effective but is worth trying if communication
overhead makes the game slow. The exercises have you explore the mitigation of
communication overhead.
***** Ex. 339 — Design and implement a communication protocol that only
transmits to the players the parts of the world that have changed in a message.
For example, when a move request is satisfied, a single ally is changed and
nothing else changes in the game. Instead of sending the entire world to all
players, only the changed ally may be sent. Is the resulting game noticeably
faster or slower?
***** Ex. 340 — Design and implement a communication protocol that does
not send every single change to the player as they happen. Instead, the server
may wait for a number of changes, n, to occur before sending a message to the
players. Is the resulting game noticeably faster or slower?
Congratulations! You have reached the end of your first step into the world of problem
solving and program design. There is still much more you can learn about problem
solving and programming, but there is no doubt that now you have a solid foundation
to continue on this journey. This journey is inevitable even for those who do not
aspire to become Computer Scientists. Remember that problem solving is at the
heart of many human activities.
Although the book builds on much of your background knowledge (e.g., high
school algebra), you likely feel you have a different understanding now of this
knowledge. The book has emphasized the use of types to organize your thoughts
during the problem solving process. At the heart of this process is how elements in the
real or an imaginary world are represented in a program. The chosen representation
may (and should) be exploited to find solutions to problems. Remember that if you
know the type of data to be processed, then you know something about what the
solution to a problem may look like.
All good things must continue . . .
Be patient and apply the skills you have learned in the future. As your studies
progress, you will discover that few textbooks on programming emphasize design.
This makes it difficult sometimes to understand the programs presented. When you
see such code, tease out the details. Try to formulate the steps of the design recipe to
enlighten you about what the program does. This a good skill to develop given that
large pieces of software evolve over years of development in which programmers
come and go. Documenting the design of a program is a service that you and others
will appreciate when you have to maintain code.
Where can you go from here? The most fundamental piece of advice is to read
and learn more about problem solving using a computer. You will be well-served
if you practice your design skills by learning about a new programming language
© The Author(s), under exclusive license to Springer Nature Switzerland AG 2022 687
M. T. Morazán, Animated Problem Solving, Texts in Computer Science,
https://doi.org/10.1007/978-3-030-85091-3_30
688 30 Advice for Future Steps
every semester and summer. The skills you have developed are directly applicable
to solving problems using programming languages beyond BSL, BSL+, and ISL+.
As you advance, you will discover abstractions that are not covered in this textbook.
You are, however, well-prepared to learn about them. You will also be well-served to
explore topics covered in this book in more depth, such as big-O notation (i.e., com-
plexity), generic programming, and distributed programming. I cannot recommend
strongly enough to take courses in the implementation of programming languages:
truly understand the technology that is central to Computer Science.
You may feel excited, overwhelmed, or both after completing this textbook. There is
no doubt that you are a better problem solver now. The truth is that you are likely to
program throughout your life. Perhaps not using a programming language as done
in this textbook, but if you are problem solving then you are programming. You may
write essays, diagnose a patient, or create a piece of music. What do these activities
have in common with programming? They process data and are refined until you are
satisfied with the result. Is this truly different than finally designing Aliens Attack 8?
Do you not refine several drafts of an essay? Use the lessons you have absorbed and
apply them to domains other than programming. The famous popular saying don’t
reinvent the wheel is a call for abstraction. If you think about it carefully, you will see
that the steps of the design recipe are applicable to problem solving in any context.
You are now in a much better position to understand and, therefore, use software.
If you use a spreadsheet, you are programming. If you adjust the settings of a
thermostat, you are programming. If you are driving, you are programming. No?
When you put your blinker on before turning, are you not sending a message to other
drivers? If you begin to realize that problem solving and programming are fully
intertwined with life, then you are now in a position to be a better problem solver.