One of the useful structures we can build with pairs is a
sequence—an ordered collection of data objects. There
are, of course, many ways to represent sequences in terms of pairs. One
particularly straightforward representation is illustrated in
figure 2.8,
where the sequence 1, 2, 3, 4 is represented as a chain of pairs. The
head
of each pair is the
corresponding item in the chain, and the
tail
of the pair is the next pair in the chain. The
tail
of the final pair signals the end of the
sequence,
represented in box-and-pointer
diagrams as a diagonal line
and in programs as
JavaScript's primitive value
null.
The entire sequence is constructed by nested
pair
operations:
The sequence 1, 2, 3, 4 represented as a chain of pairs.
ペアの連鎖として表現されたシーケンス 1, 2, 3, 4。
Such a sequence of pairs, formed by nested
pair applications,
is called a
list, and
our JavaScript environment
provides a primitive called
list to help in constructing
lists.
このような入れ子になった
pair の適用
によって形成されるペアのシーケンスは
リストと呼ばれます。
JavaScript 環境
はリストの構築を助けるプリミティブ
list を提供しています。
Our interpreter prints pairs using a textual representation of
box-and-pointer diagrams that we call box notation.
The result of pair(1, 2)
is printed as [1, 2], and
the data object in figure 2.8
is printed as
[1, [2, [3, [4, null]]]]:
We can think of
head
as selecting the first item in the list, and of
tail
as selecting the sublist consisting of all but the first item. Nested
applications of
head
and
tail
can be used to extract the second, third, and subsequent items in the
list.
head
はリストの最初の要素を選択し、
tail
は最初の要素を除いた部分リストを選択すると考えることができます。
head
と
tail
を入れ子に適用することで、リストの2番目、3番目、およびそれ以降の要素を取り出すことができます。
The constructor
pair
makes a list like the original one, but with an additional item at the
beginning.
コンストラクタ
pair
は、元のリストと同じですが先頭に要素が追加されたリストを作ります。
head(one_through_four);
1
tail(one_through_four);
[2, [3, [4, null]]]
head(tail(one_through_four));
2
pair(10, one_through_four);
[10, [1, [2, [3, [4, null]]]]]
pair(5, one_through_four);
[5, [1, [2, [3, [4, null]]]]]
The value null, used to terminate
the chain of pairs, can be thought of as a sequence of no elements, the
empty list.
Box notation is sometimes difficult to read. In this book, when we want to
indicate the list nature of a data structure, we will employ the
alternative
list notation: Whenever possible, list notation uses
applications
of list whose evaluation would result in the
desired structure. For example, instead of the box notation
ボックス記法は読みにくいことがあります。本書では、データ構造のリストとしての性質を示したいとき、代替の
リスト記法を使います。リスト記法では、可能な限り、評価すると目的の構造が得られるような list の適用を使います。たとえば、ボックス記法の
The use of pairs to represent sequences of elements as lists is accompanied
by conventional programming techniques for manipulating lists by
successively
using tail to walk down the lists.
For example, the
function
list_ref
takes as arguments a list and a number $n$ and
returns the $n$th item of the list. It is
customary to number the elements of the list beginning with 0. The method
for computing
list_ref
is the following:
For $n=0$,
list_ref
should return the
head
of the list.
Otherwise,
list_ref
should return the $(n-1)$st item of the
tail
of the list.
Often we
walk down the whole list.
To aid in this,
our JavaScript environment
includes a primitive
predicate
is_null,
which tests whether its argument is the empty list. The
function
length, which returns the number of items in
a list, illustrates this typical pattern of use:
Another conventional programming technique is to
construct an answer list by adjoining elements to
the front of the list with
pair
while walking down a list using
tail,
as in the
function
append, which takes two lists as arguments and
combines their elements to make a new list:
Consider the
change-counting program of
section 1.2.2. It would be nice to be
able to easily change the currency used by the program, so that we could
compute the number of ways to change a British pound, for example. As
the program is written, the knowledge of the currency is distributed
partly into the
function
first_denomination
and partly into the
function
count_change
(which knows
that there are five kinds of U.S. coins).
It would be nicer
to be able to supply a list of coins to be used for making change.
We want to rewrite the
function
cc so that its second argument is a list of
the values of the coins to use rather than an integer specifying which
coins to use. We could then have lists that defined each kind of
currency:
関数
cc を、第2引数がどの硬貨を使うかを指定する整数ではなく、使用する硬貨の額面のリストになるように書き換えたいと思います。そうすれば、各通貨を定義するリストを持つことができます:
To do this will require changing the program
cc somewhat. It will still have the same
form, but it will access its second argument differently, as follows:
このためには、プログラム cc を少し変更する必要があります。形は同じですが、第2引数へのアクセス方法が異なります:
Define the
functions
first_denomination,
except_first_denomination,
and
no_more
in terms of primitive operations on list structures. Does the order of
the list
coin_values
affect the answer produced by cc?
Why or why not?
function first_denomination(coin_values) {
return head(coin_values);
}
function except_first_denomination(coin_values) {
return tail(coin_values);
}
function no_more(coin_values) {
return is_null(coin_values);
}
The order of the list coin_values
does not affect the answer given by any correct solution of the problem,
because the given list represents an unordered collection of
denominations.
In the presence of higher-order functions, it is not strictly necessary
for functions to have multiple parameters; one would
suffice. If we have a function such as
plus that naturally requires two
arguments, we could write a variant of the function to which we pass
the arguments one at a time. An application of the variant to the
first argument could return a function that we can then apply to the
second argument, and so on. This practice—called
currying and named after the American mathematician and
logician
Haskell Brooks Curry—is quite common in programming
languages such as
Haskell and
OCaml. In JavaScript, a curried
version of plus looks as follows.
Write a function brooks that
takes a curried function as first argument and as second argument a list
of arguments to which the curried function is then applied, one by one,
in the given order. For example, the following application of
brooks should have the
same effect as
plus_curried(3)(4):
One extremely useful operation is to apply some transformation to each
element in a list and generate the list of results. For instance, the
following
function
scales each number in a list by a given factor:
We can abstract this general idea and capture it as a common pattern
expressed as a higher-order
function,
just as in section 1.3. The
higher-order
function
here is called map.
The function map
takes as arguments a
function
of one argument and a list, and returns a list of the results produced by
applying the
function
to each element in the list:
Now we can give a new definition of
scale_list
in terms of map:
これで map を使った
scale_list
の新しい定義を与えることができます:
function scale_list(items, factor) {
return map(x => x * factor, items);
}
The function map
is an important construct, not only because it captures a common pattern,
but because it establishes a higher level of abstraction in dealing with
lists. In the original definition of
scale_list,
the recursive structure of the program draws attention to the
element-by-element processing of the list. Defining
scale_list
in terms of map suppresses that level of
detail and emphasizes that scaling transforms a list of elements to a list
of results. The difference between the two definitions is not that the
computer is performing a different process (it isn't) but that we
think about the process differently. In effect,
map helps establish an abstraction barrier
that isolates the implementation of
functions
that transform lists from the details of how the elements of the list are
extracted and combined. Like the barriers shown in
figure 2.2,
this abstraction gives us the flexibility to change the low-level details
of how sequences are implemented, while preserving the conceptual framework
of operations that transform sequences to sequences.
Section 2.2.3 expands
on this use of sequences as a framework for organizing programs.
Louis then tries to fix his bug by interchanging the arguments to
pair:
Louis は
pair
の引数を入れ替えてバグを修正しようとします:
function square_list(items) {
function iter(things, answer) {
return is_null(things)
? answer
: iter(tail(things),
pair(answer,
square(head(things))));
}
return iter(items, null);
}
This doesn't work either. Explain.
これもうまくいきません。その理由を説明してください。
The result list is reversed in the first program because the argument
list is traversed in the given order, from first to last, but squares
are added successively to the front of the answer list via
pair.
The last element of the list is the last one to be added to the answer
and thus ends up as the first element of the result list.
The second program makes things worse! The result is not even a list
any longer, because the elements occupy the tail position of the
result list and not the head position.
2番目のプログラムはさらに悪化します!結果はもはやリストですらありません。要素が結果リストの head 位置ではなく tail 位置を占めるためです。
The
function
for_each
is similar to map. It takes as arguments a
function
and a list of elements. However, rather than forming a list of the
results,
for_each
just applies the
function
to each of the elements in turn, from left to right. The values returned by
applying the
function
to the elements are not used
at all—for_each
is used with
functions
that perform an action, such as printing. For example,
In this book, we use list to mean a chain of
pairs terminated by the end-of-list marker. In contrast, the term
list structure refers to any data structure made out of pairs,
not just to lists.
Our JavaScript environment provides
a primitive function
display_list
that works like the primitive function
display, except that
it uses list notation instead of box notation.