This section presents a simple language for drawing pictures that
illustrates the power of data abstraction and closure, and also exploits
higher-order
functions
in an essential way. The language is designed to make it easy to
experiment with patterns such as the ones in
figure 2.16, which are composed of
repeated elements that are shifted and scaled.
In this language, the data objects being
combined are represented as
functions
rather than as list structure. Just as
pair,
which satisfies the
closure property, allowed us to easily build arbitrarily complicated list
structure, the operations in this language, which also satisfy the closure
property, allow us to easily build arbitrarily complicated patterns.
When we began our study of programming in
section 1.1, we emphasized the
importance of describing a language by focusing on the language's
primitives, its means of combination, and its means of abstraction.
We'll follow that framework here.
Part of the elegance of this picture language is that there is only one
kind of element, called a
painter. A painter draws an image that is shifted and scaled to
fit within a designated
parallelogram-shaped frame. For example, there's a primitive painter
we'll call wave
that makes a crude line drawing,
as shown in figure 2.17.
The actual shape of the drawing depends on the frame—all four
images in figure 2.17 are produced by the same
wave painter, but with respect to four
different frames. Painters can be more elaborate than this: The primitive
painter called rogers paints a picture of
MIT's founder, William Barton Rogers, as shown in
figure 2.18.
Images of William Barton Rogers, founder and first
president of MIT, painted with respect to the same four frames as in
figure 2.17 (original image courtesy MIT Museum).
MIT の創設者で初代学長の William Barton Rogers の肖像。図 2.17と同じ4つのフレームに対して描かれている(原画像は MIT Museum 提供)。
To combine images, we use various
operations that construct new painters
from given painters. For example, the
beside operation takes two painters and
produces a new, compound painter that draws the first painter's image
in the left half of the frame and the second painter's image in the
right half of the frame. Similarly,
below takes two painters and produces a
compound painter that draws the first painter's image below the
second painter's image. Some operations transform a single painter
to produce a new painter. For example,
flip_vert
takes a painter and produces a painter that draws its image upside-down, and
flip_horiz
produces a painter that draws the original painter's image
left-to-right reversed.
In building up a complex image in this manner we are exploiting the fact
that painters are
closed under the language's means of combination.
The beside or
below of two painters is itself a painter;
therefore, we can use it as an element in making more complex painters.
As with building up list structure using
pair,
the closure of our data under the means of combination is crucial to the
ability to create complex structures while using only a few operations.
Once we can combine painters, we would like to be able to abstract typical
patterns of combining painters. We will implement the painter operations as
JavaScript functions.
This means that we don't need a special abstraction mechanism in the
picture language: Since the means of combination are ordinary
JavaScript functions,
we automatically have the capability to do anything with painter operations
that we can do with
functions.
For example, we can abstract the pattern in
wave4 as
The recursive operation
right_split applied to the
painters wave and
rogers.
Combining four
corner_split figures
produces symmetric
square_limit
as shown in figure 2.16.
By placing four copies of a
corner_split
appropriately, we obtain a pattern called
square_limit,
whose application to wave and
rogers is shown in
figure 2.16:
function up_split(painter, n) {
if (n === 0) {
return painter;
} else {
const smaller = up_split(painter, n - 1);
return stack(beside(smaller, smaller), painter);
}
}
Higher-order operations
高階演算
In addition to abstracting patterns of combining painters, we can work at a
higher level, abstracting patterns of combining painter operations. That
is, we can view the painter operations as elements to manipulate and can
write means of combination for these
elements—functions
that take painter operations as arguments and create new painter operations.
For example,
flipped_pairs
and
square_limit
each arrange four copies of a painter's image in a square pattern;
they differ only in how they orient the copies. One way to abstract this
pattern of painter combination is with the following
function,
which takes four one-argument painter operations and produces a painter
operation that transforms a given painter with those four operations and
arranges the results in a square.
The functions tl,
tr, bl, and
br are the transformations to apply to the
top left copy, the top right copy, the bottom left copy, and the bottom
right copy, respectively.
The functions
right_split
and
up_split
can be expressed as instances of a general splitting operation.
Declare a function
split with the property that evaluating
Before we can show how to implement painters and their means of
combination, we must first consider
frames. A frame can be described by three vectors—an origin vector
and two edge vectors. The origin vector specifies the offset of the
frame's origin from some absolute origin in the plane, and the edge
vectors specify the offsets of the frame's corners from its origin.
If the edges are perpendicular, the frame will be rectangular.
Otherwise the frame will be a more general parallelogram.
Figure 2.24 shows a frame and its associated
vectors. In accordance with data abstraction, we need not be specific yet
about how frames are represented, other than to say that there is a
constructor
make_frame,
which takes three vectors and produces a frame, and three corresponding
selectors
origin_frame,
edge1_frame,
and
edge2_frame
(see exercise 2.47).
A frame is described by three vectors—an origin and two edges.
フレームは3つのベクトル——原点と2つの辺——で記述される。
We will use coordinates in the
unit square
($0\leq x, y\leq 1$) to specify images. With
each frame, we associate a
frame coordinate map, which will be used to shift and scale images
to fit the frame. The map transforms the unit square into the frame by
mapping the vector $\mathbf{v}=(x, y)$ to the
vector sum
For example, $(0, 0)$ is mapped to the origin of
the frame, $(1, 1)$ to the vertex diagonally
opposite the origin, and $(0.5, 0.5)$ to the
center of the frame. We can create a frame's coordinate map with
the following
function:
function frame_coord_map(frame) {
return v => add_vect(origin_frame(frame),
add_vect(scale_vect(xcor_vect(v),
edge1_frame(frame)),
scale_vect(ycor_vect(v),
edge2_frame(frame))));
}
Observe that applying
frame_coord_map
to a frame returns a
function
that, given a vector, returns a vector. If the argument vector is in the
unit square, the result vector will be in the frame. For example,
A two-dimensional
vector $v$ running from the
origin to a point can be represented as a pair consisting of an
$x$-coordinate and a
$y$-coordinate. Implement a data abstraction
for vectors by giving a constructor
make_vect
and corresponding selectors
xcor_vect
and
ycor_vect.
In terms of your selectors and constructor, implement
functions
add_vect,
sub_vect,
and
scale_vect
that perform the operations vector addition, vector subtraction, and
multiplying a vector by a scalar:
function make_frame(origin, edge1, edge2) {
return list(origin, edge1, edge2);
}
function make_frame(origin, edge1, edge2) {
return pair(origin, pair(edge1, edge2));
}
For each constructor supply the appropriate selectors to produce an
implementation for frames.
各コンストラクタに対して、フレームの実装を完成させるための適切なセレクタを提供してください。
function make_frame(origin, edge1, edge2) {
return list(origin, edge1, edge2);
}
function origin_frame(frame) {
return list_ref(frame, 0);
}
function edge1_frame(frame) {
return list_ref(frame, 1);
}
function edge2_frame(frame) {
return list_ref(frame, 2);
}
function make_frame(origin, edge1, edge2) {
return pair(origin, pair(edge1, edge2));
}
function origin_frame(frame) {
return head(frame);
}
function edge1_frame(frame) {
return head(tail(frame));
}
function edge2_frame(frame) {
return tail(tail(frame));
}
Painters
ペインタ
A painter is represented as a
function
that, given a frame as argument, draws a particular image shifted and
scaled to fit the frame. That is to say, if
p is a painter and
f is a frame, then we produce
p's image in
f by calling p
with f as argument.
ペインタは、フレームを引数として受け取り、フレームに収まるようにシフト・スケーリングされた特定の画像を描く
関数
として表現されます。つまり、p がペインタで f がフレームであれば、p を引数 f で呼び出すことで、f 内に p の画像を生成します。
The details of how primitive painters are implemented depend on the
particular characteristics of the graphics system and the type of image to
be drawn. For instance, suppose we have a
function
draw_line
that draws a line on the screen between two specified points. Then we can
create painters for line drawings, such as the
wave
painter in figure 2.17, from lists of line
segments as follows:
The segments are given using coordinates with respect to the unit square.
For each segment in the list, the painter transforms the segment endpoints
with the frame coordinate map and draws a line between the transformed
points.
Representing painters as
functions
erects a powerful abstraction barrier in the picture language. We can
create and intermix all sorts of primitive painters, based on a variety of
graphics capabilities. The details of their implementation do not matter.
Any
function
can serve as a painter, provided that it takes a frame as argument and
draws something scaled to fit the frame.
A directed line segment in the plane can be represented as a pair of
vectors—the vector running from the origin to the start-point of
the segment, and the vector running from the origin to the end-point of
the segment. Use your vector representation from
exercise 2.46 to define a representation for
segments with a constructor
make_segment
and selectors
start_segment
and
end_segment.
An operation on painters (such as
flip_vert
or beside)
works by creating a painter that invokes the original painters with respect
to frames derived from the argument frame. Thus, for example,
flip_vert
doesn't have to know how a painter works in order to flip
it—it just has to know how to turn a frame upside down: The flipped
painter just uses the original painter, but in the inverted frame.
Painter operations are based on the
function
transform_painter,
which takes as arguments a painter and information on how to transform a
frame and produces a new painter. The transformed painter, when called on
a frame, transforms the frame and calls the original painter on the
transformed frame. The arguments to
transform_painter
are points (represented as vectors) that specify the corners of the new
frame: When mapped into the frame, the first point specifies the new
frame's origin and the other two specify the ends of its edge vectors.
Thus, arguments within the unit square specify a frame contained within the
original frame.
function flip_vert(painter) {
return transform_painter(painter,
make_vect(0, 1), // new origin
make_vect(1, 1), // new end of edge1
make_vect(0, 0)); // new end of edge2
}
Using
transform_painter,
we can easily define new transformations.
For example, we can declare a
painter that shrinks its image to the upper-right quarter of the frame
it is given:
Frame transformation is also the key to
defining means of combining two or more painters.
The beside
function,
for example, takes two painters, transforms them to paint in the left and
right halves of an argument frame respectively, and produces a new,
compound painter. When the compound painter is given a frame, it calls the
first transformed painter to paint in the left half of the frame and calls
the second transformed painter to paint in the right half of the frame:
Observe how the painter data abstraction, and in particular the
representation of painters as
functions,
makes
beside easy to implement. The
beside
function
need not know anything about the details of the component painters other
than that each painter will draw something in its designated frame.
Declare
the transformation
flip_horiz,
which flips painters horizontally, and transformations that rotate painters
counterclockwise by 180 degrees and 270 degrees.
function flip_horiz(painter) {
return transform_painter(painter,
make_vect(1, 0), // new origin
make_vect(0, 0), // new end of edge1
make_vect(1, 1)); // new end of edge2
}
The transformation
rotate180:
function rotate180(painter) {
return transform_painter(
painter,
make_vect(1, 1), // new origin
make_vect(0, 1), // new end of edge1
make_vect(1, 0)); // new end of edge2
}
The transformation
rotate270:
function rotate270(painter) {
return transform_painter(
painter,
make_vect(0, 1), // new origin
make_vect(0, 0), // new end of edge1
make_vect(1, 0)); // new end of edge2
}
Declare
the
below operation for painters.
The function below
takes two painters as arguments. The resulting painter, given a frame,
draws with the first painter in the bottom of the frame and with the
second painter in the top.
Define below in two different
ways—first by writing a
function
that is analogous to the
beside
function
given above, and again in terms of beside and
suitable rotation operations (from exercise 2.50).
function below(painter1, painter2) {
return rotate270(beside(rotate90(painter1),
rotate90(painter2)));
}
Levels of language for robust design
堅牢な設計のための言語のレベル
The picture language exploits some of the critical ideas we've
introduced about abstraction with
functions
and data. The fundamental data abstractions, painters, are implemented
using
functional
representations, which enables the language to handle different basic
drawing capabilities in a uniform way. The means of combination satisfy
the closure property, which permits us to easily build up complex designs.
Finally, all the tools for abstracting
functions
are available to us for abstracting means of combination for painters.
We have also obtained a glimpse of another crucial idea about languages and
program design. This is the approach of
stratified design, the notion that a complex system should be
structured as a sequence of levels that are described using a sequence of
languages. Each level is constructed by combining parts that are regarded
as primitive at that level, and the parts constructed at each level are
used as primitives at the next level. The language used at each level
of a stratified design has primitives, means of combination, and means
of abstraction appropriate to that level of detail.
Stratified design pervades the engineering of complex systems. For
example, in computer engineering, resistors and transistors are
combined (and described using a language of analog circuits) to
produce parts such as and-gates and or-gates, which form the
primitives of a language for digital-circuit design.
階層的設計は複雑なシステムの工学に広く浸透しています。たとえばコンピュータ工学では、抵抗器やトランジスタが組み合わされ(アナログ回路の言語を使って記述され)、AND ゲートや OR ゲートなどの部品が作られます。これらはデジタル回路設計の言語のプリミティブを形成します。
These parts are combined to build
processors, bus structures, and memory systems, which are in turn
combined to form computers, using languages appropriate to computer
architecture. Computers are combined to form distributed systems,
using languages appropriate for describing network interconnections,
and so on.
As a tiny example of stratification, our picture language uses primitive
elements (primitive painters) that specify points and lines to provide the
shapes of a painter like rogers. The bulk of
our description of the picture language focused on combining these
primitives, using geometric combiners such as
beside and below.
We also worked at a higher level, regarding
beside and below
as primitives to be manipulated in a language whose operations, such as
square_of_four,
capture common patterns of combining geometric combiners.
Stratified design helps make programs
robust, that is, it makes
it likely that small changes in a specification will require
correspondingly small changes in the program. For instance, suppose we
wanted to change the image based on wave
shown in figure 2.16. We could work
at the lowest level to change the detailed appearance of the
wave element; we could work at the middle
level to change the way
corner_split
replicates the wave; we could work at the
highest level to change how
square_limit
arranges the four copies of the corner. In general, each level of a
stratified design provides a different vocabulary for expressing the
characteristics of the system, and a different kind of ability to change it.
Make changes to the square limit of wave
shown in figure 2.16 by working at
each of the levels described above. In particular:
Add some segments to the primitive
wave painter
of exercise 2.49 (to add a smile, for
example).
Change the pattern constructed by
corner_split
(for example, by using only one copy of the
up_split
and
right_split
images instead of two).
Modify the version of
square_limit
that uses
square_of_four
so as to assemble the corners in a different pattern.
(For example, you might make the big Mr. Rogers look outward
from each corner of the square.)
The picture
language is based on the language
Peter Henderson created to construct images like
M.C. Escher's Square Limit woodcut (see
Henderson 1982). The woodcut incorporates a repeated
scaled pattern, similar to the arrangements drawn using the
square_limit
function
in this section.
William Barton Rogers (1804–1882) was the founder and first
president of MIT. A geologist and talented teacher, he taught at
William and Mary College and at the University of Virginia. In 1859
he moved to Boston, where he had more time for research, worked on a
plan for establishing a polytechnic institute, and
served as Massachusetts's first State Inspector of Gas Meters.
When MIT was established in 1861, Rogers was elected its first
president. Rogers espoused an ideal of useful learning
that was different from the university education of the time, with its
overemphasis on the classics, which, as he wrote, stand in the
way of the broader, higher and more practical instruction and
discipline of the natural and social sciences. This
education was likewise to be different from narrow trade-school
education. In Rogers's words:
The world-enforced distinction between the practical and the
scientific worker is utterly futile, and the whole experience of
modern times has demonstrated its utter worthlessness.
Rogers served as president of MIT until 1870, when he resigned due to
ill health. In 1878 the second president of MIT,
John Runkle, resigned under the pressure of a financial crisis
brought on by the Panic of 1873 and strain of fighting off attempts
by Harvard to take over MIT. Rogers returned to hold the office of
president until 1881.
Rogers collapsed and died while addressing MIT's graduating
class at the commencement exercises of 1882. Runkle quoted
Rogers's last words in a memorial address delivered that same
year:
As I stand here today and see what the Institute is, … I call
to mind the beginnings of science. I remember one hundred and fifty
years ago Stephen Hales published a pamphlet on the subject of
illuminating gas, in which he stated that his researches had
demonstrated that 128 grains of bituminous coal—Bituminous coal, these were his last words on
earth. Here he bent forward, as if consulting some notes on the
table before him, then slowly regaining an erect position, threw
up his hands, and was translated from the scene of his earthly
labors and triumphs to the tomorrow of death,
where the mysteries of life are solved, and the disembodied
spirit finds unending satisfaction in contemplating the new and
still unfathomable mysteries of the infinite future.
In the words of Francis A. Walker
(MIT's third president):
All his life he had borne himself most faithfully and heroically,
and he died as so good a knight would surely have wished, in
harness, at his post, and in the very part and act of public duty.
William Barton Rogers(1804–1882)は MIT の創設者であり初代学長でした。地質学者で優れた教育者であった彼は、ウィリアム・アンド・メアリー大学とバージニア大学で教鞭を執りました。1859年にボストンに移り、研究により多くの時間を割き、工科大学設立の計画に取り組み、マサチューセッツ州初のガスメーター検査官を務めました。
1861年に MIT が設立されると、Rogers は初代学長に選出されました。Rogers は当時の大学教育、すなわち古典を過度に重視する教育とは異なる有用な学問の理想を掲げました。彼が書いたように、古典は自然科学と社会科学のより広く、より高度で、より実践的な教育と訓練の妨げになっているのです。この教育はまた、狭い職業訓練学校の教育とも異なるものでした。Rogers の言葉を借りれば:
In square_of_four,
we use an extension of the syntax of lambda expressions that was introduced
in section 1.3.2: The body of a lambda
expression can be a block, not just a return expression.
Such a lambda expression has the shape
($parameters$)=>{$statements$} or
$parameter$=>{$statements$}.
The function
rotate180
rotates a painter by 180 degrees. Instead of
rotate180
we could say
compose(flip_vert, flip_horiz),
using the
compose
function
from exercise 1.42.
The function
frame_coord_map
uses the vector operations described in
exercise 2.46 below, which we assume have been
implemented using some representation for vectors. Because of data
abstraction, it doesn't matter what this vector representation is,
so long as the vector operations behave correctly.
The function
segments_to_painter
uses the representation for line segments described in
exercise 2.48 below. It also uses the
for_each
function
described in exercise 2.23.
For example, the rogers painter of
figure 2.18 was constructed from a gray-level
image. For each point in a given frame, the
rogers painter determines the point in
the image that is mapped to it under the frame coordinate map, and
shades it accordingly.
By allowing different types of painters, we are capitalizing on the
abstract data idea discussed in section 2.1.3,
where we argued that a rational-number representation could be anything at
all that satisfies an appropriate condition. Here we're using the
fact that a painter can be implemented in any way at all, so long as it
draws something in the designated frame.
Section 2.1.3 also showed how pairs could be
implemented as
functions.
Painters are our second example of a
functional
representation for data.