While the preceding part gets away with explaining local and lambda in an informal manner, the introduction of such abstraction mechanisms really requires additional terminology to facilitate such discussions. In particular, these discussions need words to delineate regions within programs and to refer to specific uses of variables.
This intermezzo starts with a section that defines the new terminology, most importantly, scope, binding variables, and bound variables. It immediately uses this new capability to introduce two abstraction mechanisms often found in programming languages:While the 2htdp/abstraction library is useful for the remaining parts of the book, the latter do not rely on any of its features. Instructors may wish to use the library anyway, though they should also explain then how the principles of design apply to languages without such features. for loops and pattern matching. The former is an alternative to functions such as map, build-list, andmap, etc; the latter abstracts over the conditional in the functions of the first three parts of the book. Both require not only the definition of functions but the creation of entirely new language constructs, meaning they are not something programmers can usually design and add to their vocabulary.
At the same time, the first occurrence of x in f is different from the others. When we evaluate (f n), the occurrence of f completely disappears while those of x are replaced with n. To distinguish these two kinds of variable occurrences, we call the x in the function header a binding occurrence and those in the function’s body the bound occurrences. We also say that the binding occurrence of x binds all occurrences of x in the body of f. Indeed, people who study programming languages even have a name for the region where a binding occurrence works, namely, its lexical scope.
The definitions of f and g bind two more names: f and g. Their scope is called top-level scope because we think of scopes as nested (see below).
h: this variable is not defined
Draw arrows from p1’s x parameter to all its bound occurrences. Draw arrows from p1 to all bound occurrences of p1. Check the results with DrRacket “Check Syntax” functionality.
In general, if the same name occurs more than once in a function, the boxes that describe the corresponding scopes never overlap. In some cases the boxes are nested within each other, which gives rise to holes. Still, the picture is always that of a hierarchy of smaller and smaller nested boxes.
; [List-of X] -> [List-of X] ; creates a version of the given list that is sorted in descending order (define (insertion-sort alon) (local ((define (sort alon) (cond [(empty? alon) '()] [else (add (first alon) (sort (rest alon)))])) (define (add an alon) (cond [(empty? alon) (list an)] [else (cond [(> an (first alon)) (cons an alon)] [else (cons (first alon) (add an (rest alon)))])]))) (sort alon)))Draw a box around the scope of each binding occurrence of sort and alon. Then draw arrows from each occurrence of sort to the matching binding occurrence. Now repeat the exercise for the following variant:Do the two functions differ other than in name?
Exercise 261. Recall that each occurrence of a variable receives its value from the corresponding binding occurrence. Consider the following definition:Where is the shaded occurrence of x bound? Since the definition is a constant definition and not a function definition, we need to evaluate the right-hand side if we wish to work with this function. What should be the value of the right-hand side according to our rules?
Exercise 262. Draw arrows from the shaded occurrences of x to their binding occurrences in each of the following three lambda expressions:Also draw a box for the scope of each shaded x and holes in the scope as necessary.
Even though it never mentions the word, Abstraction introduces loops. Abstractly, a loop traverses compound data, processing one piece at a time. In the process, loops also synthesize data. For example, map traverses a list, applies a function to each item, and collects the results in a list. Similarly, build-list enumerates the sequence of predecessors of a natural number (from 0 to (- n 1)), maps each of these to some value, and also gathers the results in a list.
The loops of ISL+ differ from those in conventional languages in two ways. First, a conventional loop does not directly create new data; in contrast, abstractions such as map and build-list are all about computing new data from traversals. Second, conventional languages often provide only a fixed number of loops; an ISL+ programmer defines new loops as needed. Put differently, conventional languages view loops as syntactic constructs akin to local or cond, and their introduction requires a detailed explanation of their vocabulary, grammar, scope, and meaning.
Loops as syntactic constructs have two advantages over functional loops. On
one hand, their shape tends to signal intentions more directly than a
composition of functions. On the other hand, language implementations
typically translate syntactic loops into faster commands for computers
than functional loops. It is therefore common that even functional
Use the 2htdp/abstraction library for this section and the next one.
In this section, we introduce a few of Racket’s so-called for loops. The goal is to illustrate how to think about conventional loops as linguistic constructs and to indicate how programs built with abstractions may use loops instead. Figure 61 spells out the grammar of our selected for loops as an extension of BSL’s grammar from Intermezzo: BSL. Every loop is an expression and, like all compound constructs, is marked with a keyword. The latter is followed by a parenthesized sequence of so-called comprehension clauses and a single expression. The clauses introduce so-called loop variables, and the expression at the end is the loop body.
expr = ... | (for/list (clause clause ...) expr) | (for*/list (clause clause ...) expr) | (for/and (clause clause ...) expr) | (for*/and (clause clause ...) expr) | (for/or (clause clause ...) expr) | (for*/or (clause clause ...) expr) | (for/sum (clause clause ...) expr) | (for*/sum (clause clause ...) expr) | (for/product (clause clause ...) expr) | (for*/product (clause clause ...) expr) | (for/string (clause clause ...) expr) | (for*/string (clause clause ...) expr) clause = (variable expr)
list, its items make up the sequence values;
natural number n, the sequence of values consists of the numbers 0, 1, ..., (- n 1);
string, its one-character strings are the sequence items.
Terminology Each evaluation of a loop body is called an iteration. Similarly, a loop is said to iterate over the values of its loop variables.
> (for/list ((i 10)) i)
(list 0 1 2 3 4 5 6 7 8 9)
> (build-list 10 (lambda (i) i))
(list 0 1 2 3 4 5 6 7 8 9)
Sample Problem: Design enumerate. The function consumes a list and produces a list of the same items paired with their relative index.
Sample Problem: Design cross. The function consumes two lists, l1 and l2, and produces pairs of all items from l1 and l2.
- .../and apply an operation like and to all of the generated values:For pragmatic reasons, the loops return the last generated value or #false if any of the results are #false.
- .../or apply an operation like or to all of the generated values:These loops return the first value that is not #false unless all the values are #false.
- .../sum add up the numbers that the iterations generate:
- .../product multiply the numbers that the iterations generate
Stop! It is an instructive exercise to re-formulate all of the above examples using the existing abstractions in ISL+. Doing so also indicates how to design functions with for loops instead of abstract functions. Hint Design and-map and or-map, which work like andmap and ormap, respectively, but return the appropriate non-#false values.
; N -> sequence? ; construct an infinite sequence of natural numbers starting at n (define (in-naturals n) ...) ; N N N -> sequence? ; construct the finite sequence of natural numbers starting with ; start ; (+ start step) ; (+ start step step) ; ... ; until the number exceeds end (define (in-range start end step) ...)
Looping over numbers isn’t always a matter of enumerating 0 through (- n 1). Often programs need to step through non-sequential sequences of numbers; other times, an unlimited supply of numbers is needed. To accommodate this form of programming, Racket comes with functions that generate sequences, and figure 62 lists two that are provided in the abstraction library for ISL+.
; N -> Number ; add the even numbers between 0 and n (exclusive) (check-expect (sum-evens 2) 0) (check-expect (sum-evens 4) 2) (define (sum-evens n) (for/sum ([i (in-range 0 n 2)]) i))
Exercise 263. Use loops to define convert-euro, a function that converts a list of US$ amounts into a list of € amounts based on an exchange rate of €1.08 per US$. Compare with exercise 231.
creates the list 0 ... (n - 1) for any natural number n;
creates the list 1 ... nfor any natural number n;
creates the list of the first n even numbers;
creates a list of lists of 0 and 1 in a diagonal arrangement, e.g.,
Define a function that ensures that no name on some list of names exceeds some given width. Compare with exercise 235.
When we design a function for a data definition with six clauses, we use a six-pronged cond expression.The 2htdp/abstraction library also supports the definition of algebraic data types with define-type and their deconstruction using type-case. The latter is like match but also ensures that the conditional has the correct number and kinds of clauses. When we formulate one of the cond clauses, we use a predicate to determine to determine whether this clause should process the given value and, if so, selectors to deconstruct any compound values. The first three parts of this book explain this idea over and over again, and many of its code snippets exhibit just this pattern.
Repeated patterns call for abstraction. While Abstraction explains how programmers can create some of these abstractions, the predicate-selector pattern can be addressed only by a language designer. In particular the designers of functional programming languages have recognized the need for abstracting the repetitive uses of predicates and selectors. These languages therefore provide pattern matching as a linguistic construct that combines and simplifies these cond clauses.
This section presents Racket’s pattern matcher as provided by the 2htdp/abstraction library. Figure 63 displays the grammatical extension for match. Clearly, match is a syntactically complex construct. While its basic outline resembles that of cond, the conditions are replaced by patterns, which come with their own vocabulary and grammatical rule.
expr = ... | (match expr [pattern expr] ...) pattern = variable | literal-constant | (cons pattern pattern) | (structure-name pattern ...) | (? predicate-name)
Syntactically, a pattern resembles a piece of nested, structural data whose leafs are literal constants, variables, or predicate patterns of the shape (? predicate-name). In the latter, predicate-name must refer to a predicate function in scope, that is, a function that consumes one value and produces a Boolean.
a literal-constant, it matches only that literal constant
> (match 4 ['four 1] ["four" 2] [#true 3] [4 "hello world"])
a variable, it matches any value, and it is associated with this value during the evaluation of the body of the corresponding match clauseSince 2 does not equal 3, the first pattern, and the second pattern is a variable, which matches any value, match picks the second clause and evaluates its body.
(structure-name pattern1 ... patternn), it matches only a structure-name structure, assuming its field values match pattern1, ..., patternn
> (define-struct phone [area switch four]) > (match (make-phone 713 664 9993) [(phone x y z) (+ x y z)])
11370Obviously, matching an instance of phone with a pattern is just like matching a cons pattern. Matching also works when several layers are involved:This match expression extracts the area code from a phone number in a list if the switch code is 664 and the last four digits are 9993.
(? predicate-name), it matches only when (predicate-name v) produces #trueThis expression produces 1, the result of the second clause, because 1 is not a symbol.
Sample Problem: Design the function last-item, which retrieves the last item on a non-empty list. Recall that non-empty lists are defined as follows:
; [Non-empty-list X] -> X ; retrieve the last item of ne-l (check-expect (last-item '(a b c)) 'c) (check-error (last-item '())) (define (last-item ne-l) (match ne-l [(cons lst '()) lst] [(cons fst rst) (last-item rst)]))
Let’s take a look at a second problem from Arbitrarily Large Data:
Sample Problem: Design the function depth, which measures the number of layers surrounding Russian doll. Recall the corresponding data definition:
Exercise 266. Use match to design drop. It consumes a list of Waters and lowers each Water by one pixel. Here is the relevant data definition:If you are stuck, start with the design of a version that uses cond.
Exercise 267. Use match to design the function replace, which substitutes the area code 713 with 281 in a list of phone records. For a structure type definition of phone records, see above. Formulate a suitable data definition first. If you are stuck, look up your solution for exercise 155.
Exercise 268. Figure 44 displays a function that determines the number of words per nested list in a list of list of strings. Using the notation from Abstraction, the function’s signature, purpose and header can be formulated like this:Define the function using match.