第6章: 関数型プログラミング

プログラムは大きくなるにつれて、複雑になり、理解しにくくなります。もちろん、私たちは皆、自分がかなり賢いと考えていますが、私たちは単なる人間であり、適度な量の混沌でさえ私たちを困惑させる傾向があります。そして、すべては下り坂になります。自分が本当に理解していないことに取り組むのは、映画によくある時限爆弾のランダムなワイヤーを切るようなものです。運が良ければ、特にあなたが映画の主人公で、適切な劇的なポーズをとれば、正しいワイヤーを切ることができるかもしれませんが、すべてを爆破する可能性は常にあります。

確かに、ほとんどの場合、プログラムを壊しても大きな爆発は起こりません。しかし、誰かの無知な tinkering によって、プログラムがエラーだらけのガタガタの塊に退化した場合、それを理に尽したものに作り直すのは大変な労力です。時には、最初からやり直した方が良い場合もあります。

したがって、プログラマーは常にプログラムの複雑さをできるだけ低く抑える方法を探しています。これを行うための重要な方法は、コードをより抽象的にすることです。プログラムを書いていると、あらゆる場面で細部に気を取られがちです。小さな問題に遭遇し、それを処理し、次の小さな問題に進みます。これは、コードをおとぎ話のように読ませます。

ええ、あなた、豆のスープを作るには、乾燥した種類のえんどう豆が必要です。そして、少なくとも一晩は水に浸しておかなければなりません。そうでなければ、何時間も何時間も煮なければなりません。私の鈍感な息子が豆のスープを作ろうとした時のことを覚えています。彼が豆を水に浸していなかったなんて信じられますか?私たちは皆、歯が折れそうになりました。とにかく、豆を水に浸したら、一人当たり約1カップ必要ですが、水に浸している間に少し膨らむので注意してください。そのため、注意しないと、何に入れていてもこぼれてしまうので、水に浸すのにも十分な水を使用しますが、私が言ったように、乾燥しているときは約1カップ、水に浸した後は、乾燥豆1カップあたり4カップの水で煮ます。2時間煮込みます。つまり、蓋をして、かろうじて調理し続け、さいの目に切った玉ねぎ、スライスしたセロリの茎、そしておそらくニンジンを1つか2つとハムを加えます。さらに数分間煮込めば、食べられます。

このレシピを別の方法で説明すると

1人あたり:乾燥したえんどう豆1カップ、みじん切りにした玉ねぎ半分、ニンジン半分、セロリの茎、そしてオプションでハム。

豆を一晩水に浸し、(1人あたり)4カップの水で2時間煮込み、野菜とハムを加えて、さらに10分間煮込みます。

これは短くなりますが、豆の浸し方がわからないと、きっと失敗して水を入れすぎてしまうでしょう。しかし、豆の浸し方は調べることができます。それがコツです。読者に一定の基本的な知識があると仮定すると、より大きな概念を扱う言語で話し、物事をはるかに短く明確な方法で表現できます。これが、多かれ少なかれ、抽象化です。

このとっぴなレシピの話は、プログラミングとどのように関係があるのでしょうか?明らかに、レシピはプログラムです。さらに、料理人が持っているはずの基本的な知識は、プログラマーが利用できる関数やその他の構成要素に対応しています。この本の序文を覚えているなら、while のようなものはループの構築を容易にし、第4章では、他の関数をより短く、より straightforward にするために、いくつかの単純な関数を記述しました。このようなツールは、言語自体によって提供されるものもあれば、プログラマーによって構築されるものもありますが、プログラムの残りの部分にある面白くない詳細の量を減らし、プログラムを扱いやすくするために使用されます。


この章の主題である関数型プログラミングは、関数を巧妙に組み合わせることで抽象化を生み出します。基本的な関数のレパートリーと、さらに重要なこととして、それらを使用する方法の知識を備えたプログラマーは、ゼロから始めるプログラマーよりもはるかに効果的です。残念ながら、標準の JavaScript 環境には、嘆かわしいほど少数の基本的な関数が付属しているため、自分で記述するか、多くの場合望ましいことですが、他の誰かのコードを利用する必要があります(第9章で詳しく説明します)。

抽象化には、他にも一般的なアプローチがあり、最も注目すべきは、第8章の主題であるオブジェクト指向プログラミングです。


少しでもセンスがあれば、気になるようになってきている醜い詳細は、配列を巡回する際、際限なく繰り返される for ループです。 for (var i = 0; i < something.length; i++) ...。これは抽象化できますか?

問題は、ほとんどの関数はいくつかの値を受け取り、それらを組み合わせて何かを返すだけですが、このようなループには実行しなければならないコードが含まれていることです。配列を巡回し、すべての要素を出力する関数を記述するのは簡単です

function printArray(array) {
  for (var i = 0; i < array.length; i++)
    print(array[i]);
}

しかし、印刷以外のことをしたい場合はどうでしょうか?「何かをする」ことは関数として表現でき、関数も値であるため、アクションを関数値として渡すことができます

function forEach(array, action) {
  for (var i = 0; i < array.length; i++)
    action(array[i]);
}

forEach(["Wampeter", "Foma", "Granfalloon"], print);

そして、無名関数を使用することで、for ループのようなものが、無駄な詳細を少なくして記述できます。

function sum(numbers) {
  var total = 0;
  forEach(numbers, function (number) {
    total += number;
  });
  return total;
}
show(sum([1, 10, 100]));

変数 total は、レキシカルスコープ規則により、無名関数内から参照できることに注意してください。また、このバージョンは for ループよりかろうじて短いわけではなく、最後にかなり不格好な }); が必要です。中括弧は無名関数の本体を閉じ、括弧はforEach への関数呼び出しを閉じ、セミコロンはこの呼び出しがステートメントであるため必要です。

配列の現在の要素にバインドされた変数 number が得られるため、numbers[i] を使用する必要はもうなく、この配列が式を評価することによって作成される場合、forEach に直接渡すことができるため、変数に格納する必要はありません。

第4章の猫コードには、次のような部分があります

var paragraphs = mailArchive[mail].split("\n");
for (var i = 0; i < paragraphs.length; i++)
  handleParagraph(paragraphs[i]);

これは次のように書くことができます...

forEach(mailArchive[mail].split("\n"), handleParagraph);

全体として、より抽象的な(または「高レベル」の)構成要素を使用すると、より多くの情報とより少ないノイズが得られます。sum のコードは、「*numbers の各数値について、その数値を合計に加算する*」と読み取られます。...「*ゼロから始まるこの変数があり、numbers と呼ばれる配列の長さまでカウントアップし、この変数のすべての値について、配列内の対応する要素を検索し、これを合計に加算する*」の代わりに。


forEach が行うのは、アルゴリズム、この場合は「配列を巡回する」を取り、それを抽象化することです。アルゴリズムの「ギャップ」、この場合はこれらの各要素に対して何をするかは、アルゴリズム関数に渡される関数によって埋められます。

他の関数で動作する関数は、高階関数と呼ばれます。関数で動作することにより、まったく新しいレベルのアクションについて話すことができます。第3章makeAddFunction 関数も高階関数です。引数として関数値を取る代わりに、新しい関数を生成します。

高階関数は、通常の関数が簡単に記述できない多くのアルゴリズムを一般化するために使用できます。これらの関数のレパートリーを自由に使えるようになると、コードについてより明確に考えるのに役立ちます。変数とループの乱雑なセットの代わりに、アルゴリズムをいくつかの基本的なアルゴリズムの組み合わせに分解できます。これらは名前で呼び出され、何度も入力する必要はありません。

*どのように*行うかではなく、*何を*したいかを記述できるということは、より高いレベルの抽象化で作業していることを意味します。実際には、これはより短く、より明確で、より快適なコードを意味します。


もう1つの便利なタイプの高階関数は、与えられた関数値を*変更*します

function negate(func) {
  return function(x) {
    return !func(x);
  };
}
var isNotNaN = negate(isNaN);
show(isNotNaN(NaN));

negate によって返される関数は、与えられた引数を元の関数 func に送り、結果を否定します。しかし、否定したい関数が複数の引数を取る場合はどうでしょうか?arguments 配列を使用して関数に渡された引数にアクセスできますが、引数の数がわからない場合はどのように関数を呼び出しますか?

関数には、apply と呼ばれるメソッドがあり、このような状況で使用されます。2つの引数を取ります。最初の引数の役割については、第8章で説明します。ここでは、null を使用します。2番目の引数は、関数を適用する必要がある引数を含む配列です。

show(Math.min.apply(null, [5, 6]));

function negate(func) {
  return function() {
    return !func.apply(null, arguments);
  };
}

残念ながら、Internet Explorer ブラウザでは、alert などの多くの組み込み関数は*実際には*関数ではありません...または何かです。typeof 演算子に渡されると、それらの型は "object" として報告され、apply メソッドがありません。独自の関数はこの問題の影響を受けず、常に実際の関数です。


配列に関連するさらにいくつかの基本的なアルゴリズムを見てみましょう。sum 関数は、実際には通常reduce または fold と呼ばれるアルゴリズムのバリアントです

function reduce(combine, base, array) {
  forEach(array, function (element) {
    base = combine(base, element);
  });
  return base;
}

function add(a, b) {
  return a + b;
}

function sum(numbers) {
  return reduce(add, 0, numbers);
}

reduce は、配列の要素とベース値を組み合わせる関数を繰り返し使用することにより、配列を単一の値に結合します。これはまさに sum が行ったことなので、reduce を使用することで短くすることができます...ただし、加算は JavaScript では演算子であり関数ではないため、最初にそれを関数に入れる必要がありました。

reduce が関数を最後の引数ではなく、最初の引数として取る理由は、一つには伝統 ― 他の言語もそのようにしている ― であり、一つにはこの章の最後で説明する特定のトリックを使うことができるからです。これは、reduce を呼び出す際に、縮小関数を無名関数として記述すると少し奇妙に見えることを意味します。なぜなら、他の引数が関数の後に続き、通常の for ブロックとの類似性が完全に失われるからです。


例 6.1

数値の配列を引数に取り、その中に含まれるゼロの数を返す関数 countZeroes を記述してください。reduce を使用してください。

次に、配列とテスト関数を引数に取り、テスト関数が true を返した配列内の要素の数を返す高階関数 count を記述してください。この関数を使用して countZeroes を再実装してください。

function countZeroes(array) {
  function counter(total, element) {
    return total + (element === 0 ? 1 : 0);
  }
  return reduce(counter, 0, array);
}

疑問符とコロンが付いた奇妙な部分は、新しい演算子を使用しています。第2章では、単項演算子と二項演算子を見てきました。これは三項演算子です ― 3つの値に対して作用します。その効果は if/else と似ていますが、if が条件付きで文を実行するのに対し、これは条件付きで式を選択します。疑問符の前の最初の部分は条件です。この条件が true の場合、疑問符の後の式、この場合は 1 が選択されます。false の場合、コロンの後の部分、この場合は 0 が選択されます。

この演算子を使用すると、コードの一部を大幅に短縮できます。内部の式が非常に大きくなったり、条件付き部分内でより多くの決定を行う必要がある場合は、通常、単純な ifelse を使用した方が読みやすくなります。

count 関数を使用したソリューションを以下に示します。最終的な countZeroes 関数をさらに短くするために、等価性テスターを生成する関数が含まれています。

function count(test, array) {
  return reduce(function(total, element) {
    return total + (test(element) ? 1 : 0);
  }, 0, array);
}

function equals(x) {
  return function(element) {return x === element;};
}

function countZeroes(array) {
  return count(equals(0), array);
}

配列に関連するもう1つの一般的に有用な「基本アルゴリズム」は、map と呼ばれます。forEach と同様に、配列を調べ、すべての要素に関数を適用します。ただし、関数によって返された値を破棄する代わりに、これらの値から新しい配列を構築します。

function map(func, array) {
  var result = [];
  forEach(array, function (element) {
    result.push(func(element));
  });
  return result;
}

show(map(Math.round, [0.01, 2, 9.89, Math.PI]));

最初の引数は function ではなく func と呼ばれていることに注意してください。これは、function がキーワードであるため、有効な変数名ではないためです。


かつて、トランシルヴァニアの深い山林に、隠遁者が住んでいました。ほとんどの時間は、山を歩き回り、木々と話したり、鳥と笑ったりしていました。しかし、時折、土砂降りの雨が彼を小さな小屋に閉じ込め、風の唸りが彼を耐え難いほど小さく感じさせると、隠遁者は何かを書きたいという衝動に駆られ、紙の上に考えを注ぎ出したくなりました。そこでは、彼自身よりも大きく成長するかもしれません。

詩、小説、哲学で惨めに失敗した後、隠遁者はついに技術書を書くことにしました。若い頃、彼はコンピュータプログラミングを少し行っていたので、それについての良い本を書くことができれば、名声と Anerkennung が確実に得られると考えました。

そこで彼は書きました。最初は木の皮の断片を使いましたが、それはあまり実用的ではないことがわかりました。彼は最寄りの村に行き、ラップトップコンピューターを買いました。数章の後、彼は自分のWebページに掲載するために、本をHTML形式にしたいと思いました...


HTMLをご存知ですか?これは、Webページにマークアップを追加するために使用される方法であり、本書では何度か使用しますので、少なくとも一般的にはどのように機能するかを知っておくとよいでしょう。あなたが優秀な生徒であれば、今すぐWebでHTMLの良い入門書を探し、それを読んだらここに戻ってくることができます。あなたのほとんどはおそらくひどい学生なので、私は簡単な説明をして、それで十分であることを願っています。

HTMLは「ハイパーテキストマークアップ言語」の略です。HTMLドキュメントはすべてテキストです。どのテキストが見出しであるか、どのテキストが紫であるかなどの情報、つまりこのテキストの構造を表現できなければならないため、JavaScript文字列のバックスラッシュのように、少数の文字に特別な意味があります。「小なり」記号と「大なり」記号は、「タグ」を作成するために使用されます。タグは、ドキュメント内のテキストに関する追加情報を提供します。たとえば、ページに画像が表示される場所をマークするために単独で使用することも、テキストや他のタグを含むこともできます。たとえば、段落の開始と終了をマークする場合です。

必須のタグがいくつかあります。HTMLドキュメント全体は常にhtmlタグの間に含まれている必要があります。HTMLドキュメントの例を次に示します

<html>
  <head>
    <title>A quote</title>
  </head>
  <body>
    <h1>A quote</h1>
    <blockquote>
      <p>The connection between the language in which we
      think/program and the problems and solutions we can imagine
      is very close.  For this reason restricting language
      features with the intent of eliminating programmer errors is
      at best dangerous.</p>
      <p>-- Bjarne Stroustrup</p>
    </blockquote>
    <p>Mr. Stroustrup is the inventor of the C++ programming
    language, but quite an insightful person nevertheless.</p>
    <p>Also, here is a picture of an ostrich:</p>
    <img src="img/ostrich.png"/>
  </body>
</html>

テキストまたは他のタグを含む要素は、最初に<tagname>で開き、その後</tagname>で終了します。 html要素には、常に2つの子要素が含まれています。headbodyです。1つ目はドキュメント*に関する*情報を、2つ目は実際のドキュメントを含んでいます。

ほとんどのタグ名は不可解な略語です。 h1は「見出し1」、最大の見出しの種類を表します。連続して小さい見出しには、h2からh6まであります。 pは「段落」を意味し、imgは「画像」を表します。 img要素にはテキストや他のタグは含まれていませんが、src="img/ostrich.png"という「属性」と呼ばれる追加情報があります。この場合、ここに表示する必要がある画像ファイルに関する情報が含まれています。

<>はHTMLドキュメントで特別な意味を持つため、ドキュメントのテキストに直接書き込むことはできません。HTMLドキュメントで「5 < 10」と記述したい場合は、「5 &lt; 10」と記述する必要があります。ここで、「lt」は「小なり」を表します。 「&gt;」は「>」に使用され、これらのコードはアンパサンド文字にも特別な意味を与えるため、単純な「&」は「&amp;」と記述されます。

さて、これらはHTMLのごく基本的なことですが、この章とHTMLドキュメントを扱う後の章を完全に混乱させることなく乗り切るには十分なはずです。


JavaScriptコンソールには、HTMLドキュメントを表示するために使用できる関数viewHTMLがあります。上記のサンプルドキュメントは変数stroustrupQuoteに保存したので、次のコードを実行して表示できます

viewHTML(stroustrupQuote);

何らかのポップアップブロッカーがブラウザにインストールまたは統合されている場合、HTMLドキュメントを新しいウィンドウまたはタブに表示しようとするviewHTMLに干渉する可能性があります。このサイトからのポップアップを許可するようにブロッカーを構成してみてください。


それで、話を 다시 시작하면、隠遁者は自分の本をHTML形式にしたかったのです。最初はすべてのタグを原稿に直接書き込んでいましたが、小なり記号と大なり記号をすべて入力すると指が痛くなり、&が必要なときに&amp;を書き忘れてしまうことがよくありました。これは彼に頭痛の種を与えました。次に、Microsoft Wordで本を書き、HTMLとして保存しようとしました。しかし、そこから出てきたHTMLは、必要以上に15倍大きく、複雑でした。その上、Microsoft Wordは彼に頭痛の種を与えました。

彼が最終的に思いついた解決策は次のとおりです。段落の区切り方と見出しの見た目に関する簡単なルールに従って、プレーンテキストで本を書きます。次に、このテキストを彼が望むとおりのHTMLに変換するプログラムを書きます。

ルールは次のとおりです

  1. 段落は空白行で区切られています。
  2. '%'記号で始まる段落は見出しです。'%'記号が多いほど、見出しは小さくなります。
  3. 段落内では、テキストの一部をアスタリスクで囲むことで強調できます。
  4. 脚注は中括弧で囲まれています。

6か月間、苦労して本と格闘した後、隠遁者はまだ数段落しか終えていませんでした。この時点で、彼の小屋は落雷に見舞われ、彼は死に、彼の執筆への野心は永遠に途絶えました。彼のラップトップの焼け焦げた残骸から、私は次のファイルを復元することができました

% The Book of Programming

%% The Two Aspects

Below the surface of the machine, the program moves. Without effort,
it expands and contracts. In great harmony, electrons scatter and
regroup. The forms on the monitor are but ripples on the water. The
essence stays invisibly below.

When the creators built the machine, they put in the processor and the
memory. From these arise the two aspects of the program.

The aspect of the processor is the active substance. It is called
Control. The aspect of the memory is the passive substance. It is
called Data.

Data is made of merely bits, yet it takes complex forms. Control
consists only of simple instructions, yet it performs difficult
tasks. From the small and trivial, the large and complex arise.

The program source is Data. Control arises from it. The Control
proceeds to create new Data. The one is born from the other, the
other is useless without the one. This is the harmonious cycle of
Data and Control.

Of themselves, Data and Control are without structure. The programmers
of old moulded their programs out of this raw substance. Over time,
the amorphous Data has crystallised into data types, and the chaotic
Control was restricted into control structures and functions.

%% Short Sayings

When a student asked Fu-Tzu about the nature of the cycle of Data and
Control, Fu-Tzu replied 'Think of a compiler, compiling itself.'

A student asked 'The programmers of old used only simple machines and
no programming languages, yet they made beautiful programs. Why do we
use complicated machines and programming languages?'. Fu-Tzu replied
'The builders of old used only sticks and clay, yet they made
beautiful huts.'

A hermit spent ten years writing a program. 'My program can compute
the motion of the stars on a 286-computer running MS DOS', he proudly
announced. 'Nobody owns a 286-computer or uses MS DOS anymore.',
Fu-Tzu responded.

Fu-Tzu had written a small program that was full of global state and
dubious shortcuts. Reading it, a student asked 'You warned us against
these techniques, yet I find them in your program. How can this be?'
Fu-Tzu said 'There is no need to fetch a water hose when the house is
not on fire.'{This is not to be read as an encouragement of sloppy
programming, but rather as a warning against neurotic adherence to
rules of thumb.}

%% Wisdom

A student was complaining about digital numbers. 'When I take the root
of two and then square it again, the result is already inaccurate!'.
Overhearing him, Fu-Tzu laughed. 'Here is a sheet of paper. Write down
the precise value of the square root of two for me.'

Fu-Tzu said 'When you cut against the grain of the wood, much strength
is needed. When you program against the grain of a problem, much code
is needed.'

Tzu-li and Tzu-ssu were boasting about the size of their latest
programs. 'Two-hundred thousand lines', said Tzu-li, 'not counting
comments!'. 'Psah', said Tzu-ssu, 'mine is almost a *million* lines
already.' Fu-Tzu said 'My best program has five hundred lines.'
Hearing this, Tzu-li and Tzu-ssu were enlightened.

A student had been sitting motionless behind his computer for hours,
frowning darkly. He was trying to write a beautiful solution to a
difficult problem, but could not find the right approach. Fu-Tzu hit
him on the back of his head and shouted '*Type something!*' The student
started writing an ugly solution. After he had finished, he suddenly
understood the beautiful solution.

%% Progression

A beginning programmer writes his programs like an ant builds her
hill, one piece at a time, without thought for the bigger structure.
His programs will be like loose sand. They may stand for a while, but
growing too big they fall apart{Referring to the danger of internal
inconsistency and duplicated structure in unorganised code.}.

Realising this problem, the programmer will start to spend a lot of
time thinking about structure. His programs will be rigidly
structured, like rock sculptures. They are solid, but when they must
change, violence must be done to them{Referring to the fact that
structure tends to put restrictions on the evolution of a program.}.

The master programmer knows when to apply structure and when to leave
things in their simple form. His programs are like clay, solid yet
malleable.

%% Language

When a programming language is created, it is given syntax and
semantics. The syntax describes the form of the program, the semantics
describe the function. When the syntax is beautiful and the semantics
are clear, the program will be like a stately tree. When the syntax is
clumsy and the semantics confusing, the program will be like a bramble
bush.

Tzu-ssu was asked to write a program in the language called Java,
which takes a very primitive approach to functions. Every morning, as
he sat down in front of his computer, he started complaining. All day
he cursed, blaming the language for all that went wrong. Fu-Tzu
listened for a while, and then reproached him, saying 'Every language
has its own way. Follow its form, do not try to program as if you
were using another language.'

善良な隠遁者の記憶を称えるために、彼のHTML生成プログラムを完成させたいと思います。この問題への良いアプローチは次のとおりです

  1. すべての空行でファイルをカットして、段落に分割します。
  2. 見出し段落から'%'文字を削除し、見出しとしてマークします。
  3. 段落自体のテキストを処理し、通常の パーツ、強調されたパーツ、脚注に分割します。
  4. すべての脚注をドキュメントの下部に移動し、数字1をその場所に残します。
  5. 各部分を正しいHTMLタグで囲みます。
  6. すべてを単一のHTMLドキュメントにまとめます。

このアプローチでは、強調されたテキスト内に脚注を挿入したり、その逆を行うことはできません。これはやや恣意的ですが、サンプルコードをシンプルに保つのに役立ちます。章末に、さらに挑戦したい場合は、プログラムを修正して「ネストされた」マークアップをサポートするようにしてみてください。

原稿全体は、文字列値として、このページで`recluseFile`関数を呼び出すことで入手できます。


アルゴリズムのステップ1は簡単です。空行は2つの改行が連続した場合に得られるものであり、第4章で見た文字列の`split`メソッドを覚えているなら、これがうまくいくことに気付くでしょう。

var paragraphs = recluseFile().split("\n\n");
print("Found ", paragraphs.length, " paragraphs.");

練習問題 6.2

`processParagraph`という関数を記述してください。この関数は、段落文字列を引数として受け取り、その段落がヘッダーかどうかを確認します。ヘッダーの場合は、'%'文字を取り除き、その数をカウントします。そして、2つのプロパティを持つオブジェクトを返します。`content`プロパティには段落内のテキストが、`type`プロパティには段落を囲むタグが含まれます。通常の段落の場合は`"p"`、'%'が1つのヘッダーの場合は`"h1"`、'%'が`X`個のヘッダーの場合は`"hX"`です。

文字列には、特定の文字を調べるために使用できる`charAt`メソッドがあることを覚えておいてください。

function processParagraph(paragraph) {
  var header = 0;
  while (paragraph.charAt(0) == "%") {
    paragraph = paragraph.slice(1);
    header++;
  }

  return {type: (header == 0 ? "p" : "h" + header),
          content: paragraph};
}

show(processParagraph(paragraphs[0]));

ここで、前に見た`map`関数を試すことができます。

var paragraphs = map(processParagraph,
                     recluseFile().split("\n\n"));

そして、あっという間に、きれいに分類された段落オブジェクトの配列ができました。しかし、アルゴリズムのステップ3を忘れて、先走りすぎました。

段落自体のテキストを処理し、通常の パーツ、強調されたパーツ、脚注に分割します。

これは以下のように分解できます。

  1. 段落がアスタリスクで始まる場合は、強調表示された部分を取り除いて保存します。
  2. 段落が中括弧で始まる場合は、脚注を取り除いて保存します。
  3. それ以外の場合は、最初の強調表示された部分または脚注まで、または文字列の末尾までの部分を取り除き、通常のテキストとして保存します。
  4. 段落に何か残っている場合は、1からやり直します。

練習問題 6.3

段落文字列を受け取り、段落フラグメントの配列を返す`splitParagraph`関数を構築します。フラグメントを表す良い方法を考えてください。

文字列内の文字または部分文字列を検索し、その位置を返す`indexOf`メソッド(見つからない場合は`-1`を返す)は、ここで何らかの形で役立つでしょう。

これはトリッキーなアルゴリズムであり、正しくない、または長すぎる記述方法はたくさんあります。問題が発生した場合は、少し考えてみてください。アルゴリズムを構成する小さなアクションを実行する内部関数を記述してみてください。

考えられる解決策を1つ示します。

function splitParagraph(text) {
  function indexOrEnd(character) {
    var index = text.indexOf(character);
    return index == -1 ? text.length : index;
  }

  function takeNormal() {
    var end = reduce(Math.min, text.length,
                     map(indexOrEnd, ["*", "{"]));
    var part = text.slice(0, end);
    text = text.slice(end);
    return part;
  }

  function takeUpTo(character) {
    var end = text.indexOf(character, 1);
    if (end == -1)
      throw new Error("Missing closing '" + character + "'");
    var part = text.slice(1, end);
    text = text.slice(end + 1);
    return part;
  }

  var fragments = [];

  while (text != "") {
    if (text.charAt(0) == "*")
      fragments.push({type: "emphasised",
                      content: takeUpTo("*")});
    else if (text.charAt(0) == "{")
      fragments.push({type: "footnote",
                      content: takeUpTo("}")});
    else
      fragments.push({type: "normal",
                      content: takeNormal()});
  }
  return fragments;
}

`takeNormal`関数では、`map`と`reduce`が過剰に使用されていることに注意してください。これは関数型プログラミングに関する章なので、関数型プログラミングを行います!これがどのように機能するかわかりますか? `map`は、指定された文字が見つかった位置の配列、または見つからない場合は文字列の末尾を生成し、`reduce`はそれらの最小値を取ります。これは、文字列内で次に確認する必要があるポイントです。

マッピングとリデュースなしでそれを書き出すと、次のようになります。

var nextAsterisk = text.indexOf("*");
var nextBrace = text.indexOf("{");
var end = text.length;
if (nextAsterisk != -1)
  end = nextAsterisk;
if (nextBrace != -1 && nextBrace < end)
  end = nextBrace;

これはさらに恐ろしいです。ほとんどの場合、一連のものに基づいて決定を下す必要がある場合、たとえ2つしかない場合でも、配列操作として記述する方が、`if`ステートメントで個々の値を処理するよりも優れています。(幸いなことに、第10章では、文字列内の「この文字またはあの文字」の最初の出現をより簡単に求める方法について説明しています。)

上記の解決策とは異なる方法でフラグメントを格納する`splitParagraph`を作成した場合は、調整が必要になる場合があります。この章の残りの部分の関数は、フラグメントが`type`および`content`プロパティを持つオブジェクトであることを前提としているためです。


これで、`processParagraph`を配線して、段落内のテキストも分割できるようになりました。私のバージョンは次のように変更できます。

function processParagraph(paragraph) {
  var header = 0;
  while (paragraph.charAt(0) == "%") {
    paragraph = paragraph.slice(1);
    header++;
  }

  return {type: (header == 0 ? "p" : "h" + header),
          content: splitParagraph(paragraph)};
}

それを段落の配列にマッピングすると、段落オブジェクトの配列が得られます。これは、フラグメントオブジェクトの配列を含んでいます。次に、脚注を取り出して、参照をその場所に配置します。次のようなものです。

function extractFootnotes(paragraphs) {
  var footnotes = [];
  var currentNote = 0;

  function replaceFootnote(fragment) {
    if (fragment.type == "footnote") {
      currentNote++;
      footnotes.push(fragment);
      fragment.number = currentNote;
      return {type: "reference", number: currentNote};
    }
    else {
      return fragment;
    }
  }

  forEach(paragraphs, function(paragraph) {
    paragraph.content = map(replaceFootnote,
                            paragraph.content);
  });

  return footnotes;
}     

`replaceFootnote`関数はすべてのフラグメントで呼び出されます。そのまま残す必要があるフラグメントを取得すると、それを返しますが、脚注を取得すると、この脚注を`footnotes`配列に格納し、代わりにその参照を返します。このプロセスでは、すべての脚注と参照にも番号が付けられます。


これで、ファイルから必要な情報を抽出するのに十分なツールができました。残っているのは、正しいHTMLを生成することだけです。

多くの人は、文字列を連結することがHTMLを生成するのに最適な方法だと考えています。たとえば、囲碁のゲームをプレイできるサイトへのリンクが必要な場合は、次のようにします。

var url = "http://www.gokgs.com/";
var text = "Play Go!";
var linkText = "<a href=\"" + url + "\">" + text + "</a>";
print(linkText);

(ここで、`a`はHTMLドキュメントでリンクを作成するために使用されるタグです。)...これは扱いにくいだけでなく、文字列`text`に山括弧またはアンパサンドが含まれている場合は、間違っています。あなたのウェブサイトでは奇妙なことが起こり、恥ずかしいほどアマチュアのように見えるでしょう。私たちはそうはなりたくないのです。いくつかの簡単なHTML生成関数は簡単に記述できます。では、それらを書いてみましょう。


HTML生成を成功させる秘訣は、HTMLドキュメントをフラットなテキストではなくデータ構造として扱うことです。JavaScriptのオブジェクトは、これをモデル化する非常に簡単な方法を提供します。

var linkObject = {name: "a",
                  attributes: {href: "http://www.gokgs.com/"},
                  content: ["Play Go!"]};

各HTML要素には、それが表すタグの名前を示す`name`プロパティが含まれています。属性がある場合は、属性が格納されているオブジェクトを含む`attributes`プロパティも含まれています。コンテンツがある場合は、この要素に含まれる他の要素の配列を含む`content`プロパティがあります。文字列はHTMLドキュメント内のテキストの役割を果たすため、配列`["Play Go!"]`はこのリンク内に1つの要素のみが含まれていることを意味します。これは単純なテキストです。

これらのオブジェクトを直接入力するのは扱いにくいですが、そうする必要はありません。これを行うためのショートカット関数を用意します。

function tag(name, content, attributes) {
  return {name: name, attributes: attributes, content: content};
}

要素の`attributes`と`content`が該当しない場合は未定義にすることができるため、この関数の2番目と3番目の引数は、必要ない場合は省略できることに注意してください。

`tag`はまだかなり原始的なので、リンクや単純なドキュメントの外部構造など、一般的なタイプの要素のショートカットを作成します。

function link(target, text) {
  return tag("a", [text], {href: target});
}

function htmlDoc(title, bodyContent) {
  return tag("html", [tag("head", [tag("title", [title])]),
                      tag("body", bodyContent)]);
}

練習問題 6.4

必要に応じてHTMLドキュメントの例を振り返り、画像ファイルの場所が与えられたときに`img` HTML要素を作成する`image`関数を記述してください。

function image(src) {
  return tag("img", [], {src: src});
}

ドキュメントを作成したら、それを文字列に縮小する必要があります。しかし、私たちが作成してきたデータ構造からこの文字列を構築することは非常に簡単です。重要なのは、ドキュメントのテキストの特殊文字を変換することを忘れないことです...

function escapeHTML(text) {
  var replacements = [[/&/g, "&amp;"], [/"/g, "&quot;"],
                      [/</g, "&lt;"], [/>/g, "&gt;"]];
  forEach(replacements, function(replace) {
    text = text.replace(replace[0], replace[1]);
  });
  return text;
}

文字列の`replace`メソッドは、最初の引数のパターンのすべての出現が2番目の引数に置き換えられた新しい文字列を作成します。そのため、`"Borobudur".replace(/r/g, "k")`は`"Bokobuduk"`になります。ここでのパターン構文については心配しないでください。第10章で説明します。`escapeHTML`関数は、行わなければならないさまざまな置換を配列に入れ、それらをループして引数に1つずつ適用できるようにします。

二重引用符も置き換えられます。これは、HTMLタグの属性内のテキストにもこの関数を使用するためです。これらは二重引用符で囲まれているため、内部に二重引用符があってはなりません。

`replace`を4回呼び出すということは、コンピューターがコンテンツをチェックして置き換えるために文字列全体を4回処理する必要があることを意味します。これはあまり効率的ではありません。十分に気にするなら、この関数のより複雑なバージョン、前に見た`splitParagraph`関数に似たものを作成して、1回だけ処理することができます。今のところ、私たちは dafür zu faul です。繰り返しますが、第10章では、これを行うためのより良い方法を示しています。


HTML要素オブジェクトを文字列に変換するには、次のような再帰関数を使用できます。

function renderHTML(element) {
  var pieces = [];

  function renderAttributes(attributes) {
    var result = [];
    if (attributes) {
      for (var name in attributes) 
        result.push(" " + name + "=\"" +
                    escapeHTML(attributes[name]) + "\"");
    }
    return result.join("");
  }

  function render(element) {
    // Text node
    if (typeof element == "string") {
      pieces.push(escapeHTML(element));
    }
    // Empty tag
    else if (!element.content || element.content.length == 0) {
      pieces.push("<" + element.name +
                  renderAttributes(element.attributes) + "/>");
    }
    // Tag with content
    else {
      pieces.push("<" + element.name +
                  renderAttributes(element.attributes) + ">");
      forEach(element.content, render);
      pieces.push("</" + element.name + ">");
    }
  }

  render(element);
  return pieces.join("");
}

HTMLタグ属性を作成するために、JavaScriptオブジェクトからプロパティを抽出する`in`ループに注意してください。また、2か所で、配列を使用して文字列を累積し、それらを結合して単一の結果文字列にしていることに注意してください。なぜ空の文字列から始めて、`+=`演算子でコンテンツを追加しなかったのでしょうか?

新しい文字列、特に大きな文字列を作成するには、かなりの作業が必要になることがわかりました。JavaScriptの文字列値は決して変更されないことを忘れないでください。何かを連結すると、新しい文字列が作成され、古い文字列はそのまま残ります。小さな文字列をたくさん連結して大きな文字列を作成する場合、すべてのステップで新しい文字列を作成する必要があり、次の部分が連結されると破棄されます。一方、すべての小さな文字列を配列に格納してから結合すると、大きな文字列は* 1つ*だけ作成する必要があります。


では、このHTML生成システムを試してみましょう...

print(renderHTML(link("http://www.nedroid.com", "Drawings!")));

うまくいったようです。

var body = [tag("h1", ["The Test"]),
            tag("p", ["Here is a paragraph, and an image..."]),
            image("img/sheep.png")];
var doc = htmlDoc("The Test", body);
viewHTML(renderHTML(doc));

さて、このアプローチは完璧ではないことを警告しておくべきでしょう。実際にレンダリングされるのはXMLであり、これはHTMLに似ていますが、より構造化されています。上記の単純なケースでは、これは問題を引き起こしません。しかし、正しいXMLではあるものの、適切なHTMLではないものがあり、これらは私たちが作成するドキュメントを表示しようとするブラウザを混乱させる可能性があります。たとえば、ドキュメントに空のscriptタグ(JavaScriptをページに挿入するために使用されます)がある場合、ブラウザはそれが空であることを認識せず、その後のすべてがJavaScriptであると見なします。(この場合、タグ内に単一のスペースを入れて空ではなくし、適切な終了タグを取得することで問題を解決できます。)


例 6.5

renderFragment関数を作成し、それを使用して別の関数renderParagraphを実装します。この関数は、段落オブジェクト(脚注は既にフィルタリングされています)を受け取り、正しいHTML要素(段落オブジェクトのtypeプロパティに応じて、段落またはヘッダーになります)を生成します。

この関数は、脚注参照をレンダリングする際に役立つ場合があります。

function footnote(number) {
  return tag("sup", [link("#footnote" + number,
                          String(number))]);
}

supタグはその内容を「上付き文字」として表示します。つまり、他のテキストよりも小さく、少し上に表示されます。リンクのターゲットは"#footnote1"のようになります。「#」文字を含むリンクは、ページ内の「アンカー」を参照します。この場合、脚注リンクをクリックすると、読者がページの下部(脚注がある場所)に移動するようにするために使用します。

強調表示されたフラグメントをレンダリングするためのタグはemであり、通常のテキストは追加のタグなしでレンダリングできます。

function renderParagraph(paragraph) {
  return tag(paragraph.type, map(renderFragment,
                                 paragraph.content));
}

function renderFragment(fragment) {
  if (fragment.type == "reference")
    return footnote(fragment.number);
  else if (fragment.type == "emphasised")
    return tag("em", [fragment.content]);
  else if (fragment.type == "normal")
    return fragment.content;
}

もうすぐ終わりです。レンダリング関数がまだないのは脚注だけです。"#footnote1"リンクを機能させるには、各脚注にアンカーを含める必要があります。HTMLでは、アンカーはa要素で指定されます。これはリンクにも使用されます。この場合、hrefの代わりにname属性が必要です。

function renderFootnote(footnote) {
  var number = "[" + footnote.number + "] ";
  var anchor = tag("a", [number], {name: "footnote" + footnote.number});
  return tag("p", [tag("small", [anchor, footnote.content])]);
}

では、正しい形式のファイルとドキュメントタイトルが与えられた場合に、HTMLドキュメントを返す関数を以下に示します。

function renderFile(file, title) {
  var paragraphs = map(processParagraph, file.split("\n\n"));
  var footnotes = map(renderFootnote,
                      extractFootnotes(paragraphs));
  var body = map(renderParagraph, paragraphs).concat(footnotes);
  return renderHTML(htmlDoc(title, body));
}

viewHTML(renderFile(recluseFile(), "The Book of Programming"));

配列のconcatメソッドを使用して、別の配列を連結できます。これは、+演算子が文字列に対して行う操作に似ています。


この章以降では、mapreduceなどの基本的な高階関数は常に利用可能であり、コード例で使用されます。時々、このセットに新しい便利なツールが追加されます。第9章では、この「基本」関数のセットに対するより構造化されたアプローチを開発します。


高階関数を使用する場合、JavaScriptでは演算子が関数ではないことがしばしば面倒です。いくつかのポイントでaddまたはequals関数が必要でした。同意していただけると思いますが、これらを毎回書き直すのは苦痛です。今後、これらの関数を含むopと呼ばれるオブジェクトが存在すると仮定します。

var op = {
  "+": function(a, b){return a + b;},
  "==": function(a, b){return a == b;},
  "===": function(a, b){return a === b;},
  "!": function(a){return !a;}
  /* and so on */
};

そのため、配列の合計を計算するためにreduce(op["+"], 0, [1, 2, 3, 4, 5])と書くことができます。しかし、引数の1つに既に値があるequalsmakeAddFunctionのようなものが必要な場合はどうでしょうか?その場合、再び新しい関数を記述することになります。

そのような場合、「部分適用」と呼ばれるものが役立ちます。引数の一部を既に知っている新しい関数を作成し、渡された追加の引数をこれらの固定引数の後続として扱いたいと考えています。これは、関数のapplyメソッドを創造的に使用することで実現できます。

function asArray(quasiArray, start) {
  var result = [];
  for (var i = (start || 0); i < quasiArray.length; i++)
    result.push(quasiArray[i]);
  return result;
}

function partial(func) {
  var fixedArgs = asArray(arguments, 1);
  return function(){
    return func.apply(null, fixedArgs.concat(asArray(arguments)));
  };
}

複数の引数を同時にバインドできるようにしたいので、argumentsオブジェクトから通常の配列を作成するには、asArray関数が必要です。その内容を実際の配列にコピーするため、concatメソッドを dessus utiliser できます。また、オプションの2番目の引数を取り、これを使用して先頭のいくつかの引数を省略できます。

また、外部関数(partial)のargumentsを別の名前の変数に格納する必要があることに注意してください。そうしないと、内部関数はそれらを見ることができません。内部関数には独自のarguments変数があり、外部関数の変数をシャドウイングします。

これで、equals(10)は、特殊なequals関数を使用せずに、partial(op["=="], 10)と書くことができます。そして、次のようなことができます。

show(map(partial(op["+"], 1), [0, 2, 4, 6, 8, 10]));

mapが配列引数の前に関数引数を取る理由は、関数を与えてmapを部分的に適用すると便利な場合が多いためです。これは、単一の値に対して動作する関数から、値の配列に対して動作する関数に「持ち上げます」。たとえば、数値の配列の配列があり、それらすべてを2乗したい場合は、次のようにします。

function square(x) {return x * x;}

show(map(partial(map, square), [[10, 100], [12, 16], [0, 1]]));

関数を組み合わせたいときに役立つ最後のトリックは、関数合成です。この章の冒頭で、関数を呼び出した結果にブール値の*not*演算子を適用するnegate関数を示しました。

function negate(func) {
  return function() {
    return !func.apply(null, arguments);
  };
}

これは、一般的なパターンの特殊なケースです。関数Aを呼び出し、次に結果に関数Bを適用します。合成は数学における一般的な概念です。これは、次のような高階関数で捉えることができます。

function compose(func1, func2) {
  return function() {
    return func1(func2.apply(null, arguments));
  };
}

var isUndefined = partial(op["==="], undefined);
var isDefined = compose(op["!"], isUndefined);
show(isDefined(Math.PI));
show(isDefined(Math.PIE));

ここでは、functionキーワードをまったく使用せずに新しい関数を定義しています。これは、たとえば、mapまたはreduceに渡す単純な関数を作成する必要がある場合に役立ちます。ただし、関数がこれらの例よりも複雑になると、通常はfunctionを使用して書き出す方が短くなります(効率的であることは言うまでもありません)。

  1. このように...