実例によるPureScript

ウェブのための関数型プログラミング

Phil Freeman, "PureScript by Example - Functional Programming for the Web"

目次に戻る

第3章 関数とレコード

3.1 この章の目標

この章では、関数およびレコードというPureScriptプログラムのふたつの構成要素を導入します。さらに、どのようにPureScriptプログラムを構造化するのか、どのように型をプログラム開発に役立てるかを見ていきます。

連絡先のリストを管理する簡単​​な住所録アプリケーションを作成していきます。このコード例により、PureScriptの構文からいくつかの新しい概念を導入します。

このアプリケーションのフロントエンドは対話式処理系 PSCiを使うようにしていますが、JavaScriptでフロントエンドを書くこともできるでしょう。実際に後の章で、フォームの検証と保存および復元の機能追加について詳しく説明します。

3.2 プロジェクトの準備

この章のソースコードは src/Data/AddressBook.pursというファイルに含まれています。このファイルは次のようなモジュール宣言とインポート一覧から始まります。

module Data.AddressBook where

import Prelude

import Control.Plus (empty)
import Data.List (List(..), filter, head)
import Data.Maybe (Maybe)

ここでは、いくつかのモジュールをインポートします。

このモジュールのインポート内容が括弧内で明示的に列挙されていることに注目してください。明示的な列挙はインポート内容の衝突を避けるのに役に立つので、一般に良い習慣です。

ソースコードリポジトリを複製したと仮定すると、この章のプロジェクトは次のコマンドを使用してPulpを使用して構築できます。

$ cd chapter3
$ bower update
$ pulp build

3.3 単純な型

JavaScriptのプリミティブ型に対応する組み込みデータ型として、PureScriptでは数値型と文字列型、真偽型の3つが定義されており、それぞれ NumberStringBooleanと呼ばれています。これらの型はすべてのモジュールに暗黙にインポートされる Primモジュールで定義されています。pulp repl:typeコマンドを使用すると、簡単な値の型を確認できます。

$ pulp repl

> :type 1.0
Number

> :type "test"
String

> :type true
Boolean

PureScriptには他にも、配列とレコード、関数などの組み込み型が定義されています。

整数は、小数点以下を省くことによって、型 Numberの浮動小数点数の値と区別されます。

> :type 1
Int

二重引用符を使用する文字列リテラルとは異なり、文字リテラルは一重引用符で囲みます。

> :type 'a'
Char

配列はJavaScriptの配列に対応していますが、JavaScriptの配列とは異なり、PureScriptの配列のすべての要素は同じ型を持つ必要があります。

> :type [1, 2, 3]
Array Int

> :type [true, false]
Array Boolean

> :type [1, false]
Could not match type Int with Boolean.

最後の例で起きているエラーは型検証器によって報告されたもので、配列の2つの要素の型を単一化(Unification)しようとして失敗したこと示しています。

レコードはJavaScriptのオブジェクトに対応しており、レコードリテラルはJavaScriptのオブジェクトリテラルと同じ構文になっています。

> author = { name: "Phil", interests: ["Functional Programming", "JavaScript"] }

> :type author
{ name :: String
, interests :: Array String
}

この型が示しているのは、オブジェクト authorは、

という2つのフィールド(field)を持っているということです。

レコードのフィールドは、ドットに続けて参照したいフィールドのラベルを書くと参照することができます。

> author.name
"Phil"

> author.interests
["Functional Programming","JavaScript"]

PureScriptの関数はJavaScriptの関数に対応しています。PureScriptの標準ライブラリは多くの関数の例を提供しており、この章ではそれらをもう少し詳しく見ていきます。

> import Prelude
> :type flip
forall a b c. (a -> b -> c) -> b -> a -> c

> :type const
forall a b. a -> b -> a

ファイルのトップレベルでは、等号の直前に引数を指定することで関数を定義することができます。

add :: Int -> Int -> Int
add x y = x + y

バックスラッシュに続けて空白文字で区切られた引数名のリストを書くことで、関数をインラインで定義することもできます。PSCiで複数行の宣言を入力するには、 :pasteコマンドを使用して"paste mode"に入ります。このモードでは、Control-Dキーシーケンスを使用して宣言を終了します。

> :paste
… add :: Int -> Int -> Int
… add = \x y -> x + y
… ^D

PSCiでこの関数が定義されていると、次のように関数の隣に2つの引数を空白で区切って書くことで、関数をこれらの引数に適用(apply)することができます。

> add 10 20
30

3.4 量化された型

前の節ではPreludeで定義された関数の型をいくつか見てきました。たとえば flip関数は次のような型を持っていました。

> :type flip
forall a b c. (a -> b -> c) -> b -> a -> c

この forallキーワードは、 flip全称量化された型(universally quantified type)を持っていることを示しています。これは、 abcをどの型に置き換えても、 flipはその型でうまく動作するという意味です。

例えば、 aIntbStringcStringというように選んでみたとします。この場合、 flipの型を次のように特殊化(specialize)することができます。

(Int -> String -> String) -> String -> Int -> String

量化された型を特殊化したいということをコードで示す必要はありません。特殊化は自動的に行われます。たとえば、すでにその型の flipを持っていたかのように、次のように単に flipを使用することができます。

> flip (\n s -> show n <> s) "Ten" 10

"10Ten"

abcの型はどんな型でも選ぶことができるといっても、型の不整合は生じないようにしなければなりません。 flipに渡す関数の型は、他の引数の型と整合性がなくてはなりません。第2引数として文字列 "Ten"、第3引数として数 10を渡したのはそれが理由です。もし引数が逆になっているとうまくいかないでしょう。

> flip (\n s -> show n <> s) 10 "Ten"

Could not match type Int with type String

3.5 字下げについての注意

JavaScriptとは異なり、PureScriptのコードは字下げの大きさに影響されます(indentation-sensitive)。これはHaskellと同じようになっています。コード内の空白の多寡は無意味ではなく、Cのような言語で中括弧によってコードのまとまりを示しているように、PureScriptでは空白がコードのまとまりを示すのに使われているということです。

宣言が複数行にわたる場合は、2つめの行は最初の行の字下げより深く字下げしなければなりません。

したがって、次は正しいPureScriptコードです。

add x y z = x +
  y + z

しかし、次は正しいコードではありません。

add x y z = x +
y + z

後者では、PureScriptコンパイラはそれぞれの行ごとにひとつ、つまり2つの宣言であると構文解析します。

一般に、同じブロック内で定義された宣言は同じ深さで字下げする必要があります。例えば PSCiでlet文の宣言は同じ深さで字下げしなければなりません。次は正しいコードです。

> :paste
… x = 1
… y = 2
… ^D

しかし、これは正しくありません。

> :paste
… x = 1
…  y = 2
… ^D

PureScriptのいくつかの予約語(例えば whereoflet)は新たなコードのまとまりを導入しますが、そのコードのまとまり内の宣言はそれより深く字下げされている必要があります。

example x y z = foo + bar
  where
    foo = x * y
    bar = y * z

ここで foobarの宣言は exampleの宣言より深く字下げされていることに注意してください。

ただし、ソースファイルの先頭、最初の module宣言における予約語 whereだけは、この規則の唯一の例外になっています。

3.6 独自の型の定義

PureScriptで新たな問題に取り組むときは、まずはこれから扱おうとする値の型の定義を書くことから始めるのがよいでしょう。最初に、住所録に含まれるレコードの型を定義してみます。

type Entry = { firstName :: String, lastName :: String, address :: Address }

これは Entryという型同義語(type synonym、型シノニム)を定義しています。 型 Entryは等号の右辺と同じ型ということです。レコードの型はいずれも文字列である firstNamelastNamephoneという3つのフィールドからなります。前者の2つのフィールドは型 Stringを持ち、 addressは以下のように定義された型 Addressを持っています。

type Address = { street :: String, city :: String, state :: String }

それでは、2つめの型同義語も定義してみましょう。住所録のデータ構造としては、単に項目の連結リストとして格納することにします。

type AddressBook = List Entry

List EntryArray Entryとは同じではないということに注意してください。 Array Entryは住所録の項目の配列を意味しています。

3.7 型構築子と種

List型構築子(type constructor、型コンストラクタ)の一例になっています。 Listそのものは型ではなく、何らかの型 aがあるとき List aが型になっています。つまり、 List型引数(type argument)aをとり、新たな型 List aを構築するのです。

ちょうど関数適用と同じように、型構築子は他の型に並べることで適用されることに注意してください。型 List Entryは実は型構築子 Listが型 Entry適用されたものです。これは住所録項目のリストを表しています。

(型注釈演算子 ::を使って)もし型 Listの値を間違って定義しようとすると、今まで見たことのないような種類のエラーが表示されるでしょう。

> import Data.List
> Nil :: List
In a type-annotated expression x :: t, the type t must have kind Type

これは種エラー(kind error)です。値がそので区別されるのと同じように、型はその(kind)によって区別され、間違った型の値が型エラーになるように、間違った種の型は種エラーを引き起こします。

NumberStringのような、値を持つすべての型の種を表す Typeと呼ばれる特別な種があります。

型構築子にも種があります。たとえば、種 Type -> Typeはちょうど Listのような型から型への関数を表しています。ここでエラーが発生したのは、値が種 Typeであるような型を持つと期待されていたのに、 Listは種 Type -> Typeを持っているためです。

PSCiで型の種を調べるには、 :kind命令を使用します。例えば次のようになります。

> :kind Number
Type

> import Data.List
> :k List
Type -> Type

> :kind List String
Type

PureScriptの種システムは他にも面白い種に対応していますが、それらについては本書の他の部分で見ていくことになるでしょう。

3.8 住所録の項目の表示

それでは最初に、文字列で住所録の項目を表現するような関数を書いてみましょう。まずは関数に型を与えることから始めます。型の定義は省略することも可能ですが、ドキュメントとしても役立つので型を書いておくようにすると良いでしょう。型宣言は関数の名前とその型を ::記号で区切るようにして書きます。

showEntry :: Entry -> String

showEntryは引数として Entryを取り stringを返す関数であるということを、この型シグネチャは言っています。 showEntryの定義は次のとおりです。

showEntry entry = entry.lastName <> ", " <>
                  entry.firstName <> ": " <> 
                  showAddress entry.address

この関数は Entryレコードの3つのフィールドを連結し、単一の文字列にします。ここで使用される showAddressaddressフィールドを連接し、単一の文字列にする関数です。 showAddressの定義は次のとおりです。

showAddress :: Address -> String
showAddress addr = addr.street <> ", " <>
                   addr.city <> ", " <>
                   addr.state

関数定義は関数の名前で始まり、引数名のリストが続きます。関数の結果は等号の後ろに定義します。フィールドはドットに続けてフィールド名を書くことで参照することができます。PureScriptでは、文字列連結はJavaScriptのような単一のプラス記号ではなく、ダイアモンド演算子( <>)を使用します。

3.9 はやめにテスト、たびたびテスト

PSCi対話式処理系では反応を即座に得られるので、試行錯誤を繰り返したいときに向いています。それではこの最初の関数が正しく動作するかを PSCiを使用して確認してみましょう。

まず、これまで書かれたコードをビルドします。

$ pulp build

次に、 PSCiを起動し、この新しいモジュールをインポートするために import命令を使います。

$ pulp repl

> import Data.AddressBook

レコードリテラルを使うと、住所録の項目を作成することができます。レコードリテラルはJavaScriptの無名オブジェクトと同じような構文で名前に束縛します。

> address = { street: "123 Fake St.", city: "Faketown", state: "CA" }

​それでは、この例に関数を適用してみてください。

> showAddress address

"123 Fake St., Faketown, CA"

そして、例で作成した addressを含む住所録の entryレコードを作成し showEntryに適用させましょう。

> entry = { firstName: "John", lastName: "Smith", address: address }
> showEntry entry

"Smith, John: 123 Fake St., Faketown, CA"

3.10 住所録の作成

今度は住所録の操作を支援する関数をいくつか書いてみましょう。空の住所録を表す値として、空のリストを使います。

emptyBook :: AddressBook
emptyBook = empty

既存の住所録に値を挿入する関数も必要でしょう。この関数を insertEntryと呼ぶことにします。関数の型を与えることから始めましょう。

insertEntry :: Entry -> AddressBook -> AddressBook

insertEntryは、最初の引数として Entry、第二引数として AddressBookを取り、新しい AddressBookを返すということを、この型シグネチャは言っています。

既存の AddressBookを直接変更することはしません。その代わりに、同じデータが含まれている新しい AddressBookを返すようにします。このように、 AddressBook永続データ構造(persistent data structure)の一例となっています。これはPureScriptにおける重要な考え方です。変更はコードの副作用であり、コードの振る舞いについての判断するのを難しくします。そのため、我々は可能な限り純粋な関数や不変のデータを好むのです。

Data.ListCons関数を使用すると insertEntryを実装できます。 PSCiを起動し :typeコマンドを使って、この関数の型を見てみましょう。

$ pulp repl

> import Data.List
> :type Cons

forall a. a -> List a -> List a

Consは、なんらかの型 aの値と、型 aを要素に持つリストを引数にとり、同じ型の要素を持つ新しいリストを返すということを、この型シグネチャは言っています。 aEntry型として特殊化してみましょう。

Entry -> List Entry -> List Entry

しかし、 List Entryはまさに AddressBookですから、次と同じになります。

Entry -> AddressBook -> AddressBook

今回の場合、すでに適切な入力があります。 EntryAddressBookConsを適用すると、新しい AddressBookを得ることができます。これこそまさに私たちが求めていた関数です!

insertEntryの実装は次のようになります。

insertEntry entry book = Cons entry book

等号の左側にある2つの引数 entrybookがスコープに導入されますから、これらに Cons関数を適用して結果の値を作成しています。

3.11 カリー化された関数

PureScriptでは、関数は常にひとつの引数だけを取ります。 insertEntry関数は2つの引数を取るように見えますが、これは実際にはカリー化された関数(curried function)の一例となっています。

insertEntryの型に含まれる ->は右結合の演算子であり、つまりこの型はコンパイラによって次のように解釈されます。

Entry -> (AddressBook -> AddressBook)

すなわち、 insertEntryは関数を返す関数である、ということです!この関数は単一の引数 Entryを取り、それから単一の引数 AddressBookを取り新しい AddressBookを返す新しい関数を返すのです。

これは例えば、最初の引数だけを与えると insertEntry部分適用(partial application)できることを意味します。 PSCiでこの結果の型を見てみましょう。

> :type insertEntry example

AddressBook -> AddressBook

期待したとおり、戻り値の型は関数になっていました。この結果の関数に、ふたつめの引数を適用することもできます。

> :type (insertEntry example) emptyBook
AddressBook

ここで括弧は不要であることにも注意してください。次の式は同等です。

> :type insertEntry example emptyBook
AddressBook

これは関数適用が左結合であるためで、なぜ単に空白で区切るだけで関数に引数を与えることができるのかも説明にもなっています。

本書では今後、「2引数の関数」というように表現することがあることに注意してください。これはあくまで、最初の引数を取り別の関数を返す、カリー化された関数を意味していると考えてください。

今度は insertEntryの定義について考えてみます。

insertEntry :: Entry -> AddressBook -> AddressBook
insertEntry entry book = Cons entry book

もし式の右辺に明示的に括弧をつけるなら、 (Cons entry)bookとなります。 insertEntry entryはその引数が単に関数 (Cons entry)に渡されるような関数だということです。この2つの関数はどんな入力についても同じ結果を返しますから、つまりこれらは同じ関数です!よって、両辺から引数 bookを削除できます。

insertEntry :: Entry -> AddressBook -> AddressBook
insertEntry entry = Cons entry

そして、同様の理由で両辺から entryも削除することができます。

insertEntry :: Entry -> AddressBook -> AddressBook
insertEntry = Cons

この処理はイータ変換(eta conversion)と呼ばれ、引数を参照することなく関数を定義するポイントフリー形式(point-free form)へと関数を書き換えるのに使うことができます。

insertEntryの場合には、イータ変換によって「 insertEntryは単にリストに対する consだ」と関数の定義はとても明確になりました。しかしながら、常にポイントフリー形式のほうがいいのかどうかには議論の余地があります。

3.12 あなたの住所録は?

最小限の住所録アプリケーションの実装で必要になる最後の関数は、名前で人を検索し適切な Entryを返すものです。これは小さな関数を組み合わせることでプログラムを構築するという、関数型プログラミングで鍵となる考え方のよい応用例になるでしょう。

まずは住所録をフィルタリングし、該当する姓名を持つ項目だけを保持するようにするのがいいでしょう。それから、結果のリストの先頭の(head)要素を返すだけです。

この大まかな仕様に従って、この関数の型を計算することができます。まず PSCiを起動し、 filter関数と head関数の型を見てみましょう。

$ pulp repl

> import Data.List
> :type filter

forall a. (a -> Boolean) -> List a -> List a

> :type head

forall a. List a -> Maybe a

型の意味を理解するために、これらの2つの型の一部を取り出してみましょう。

filterはカリー化された2引数の関数です。最初の引数は、リストの要素を取り Boolean値を結果として返す関数です。第2引数は要素のリストで、返り値は別のリストです。

headは引数としてリストをとり、 Maybe aという今まで見たことがないような型を返します。 Maybe aは型 aのオプショナルな値、つまり aの値を持つか持たないかのどちらかの値を示しており、JavaScriptのような言語で値がないことを示すために使われる nullの型安全な代替手段を提供します。これについては後の章で詳しく扱います。

filterheadの全称量化された型は、PureScriptコンパイラによって次のように特殊化(specialized)されます。

filter :: (Entry -> Boolean) -> AddressBook -> AddressBook

head :: AddressBook -> Maybe Entry

検索する関数の引数として姓と名前を渡す必要があるのもわかっています。

filterに渡す関数も必要になることもわかります。この関数を filterEntryと呼ぶことにしましょう。 filterEntryEntry -> Booleanという型を持っています。 filter filterEntryという関数適用の式は、 AddressBook -> AddressBookという型を持つでしょう。もしこの関数の結果を head関数に渡すと、型 Maybe Entryの結果を得ることになります。

これまでのことをまとめると、この findEntry関数の妥当な型シグネチャは次のようになります。

findEntry :: String -> String -> AddressBook -> Maybe Entry

findEntryは、姓と名前の2つの文字列、および AddressBookを引数にとり、 Maybe Entryという型の値を結果として返すということを、この型シグネチャは言っています。結果の Maybe Entryという型は、名前が住所録で発見された場合にのみ Entryの値を持ちます。

そして、 findEntryの定義は次のようになります。

findEntry firstName lastName book = head $ filter filterEntry book
  where
    filterEntry :: Entry -> Boolean
    filterEntry entry = entry.firstName == firstName && entry.lastName == lastName

一歩づつこのコードの動きを調べてみましょう。

findEntryは、どちらも文字列型である firstNamelastNameAddressBook型の bookという3つの名前をスコープに導入します

定義の右辺では filter関数と head関数が組み合わされています。まず項目のリストをフィルタリングし、その結果に head関数を適用しています。

真偽型を返す関数 filterEntrywhere節の内部で補助的な関数として定義されています。このため、 filterEntry関数はこの定義の内部では使用できますが、外部では使用することができません。また、 filterEntryはそれを包む関数の引数に依存することができ、 filterEntryは指定された Entryをフィルタリングするために引数 firstNamelastNameを使用しているので、 filterEntryfindEntryの内部にあることは必須になっています。

最上位での宣言と同じように、必ずしも filterEntryの型シグネチャを指定しなくてもよいことに注意してください。ただし、ドキュメントとしても役に立つので型シグネチャを書くことは推奨されています。

3.13 中置の関数適用

上でみた findEntryのコードでは、少し異なる形式の関数適用が使用されています。 head関数は中置の $演算子を使って式 filter filterEntry bookに適用されています。

これは head (filter filterEntry book)という通常の関数適用と同じ意味です。

($)はPreludeで定義されている apply関数の別名で、次のように定義されています。

apply :: forall a b. (a -> b) -> a -> b
apply f x = f x

infixr 0 apply as $

ここで、 applyは関数と値をとり、その値にその関数を適用します。 infixrキーワードは ($)applyの別名として定義します。

しかし、なぜ通常の関数適用の代わりに $を使ったのでしょうか? その理由は $は右結合で優先順位の低い演算子だということにあります。これは、深い入れ子になった関数適用のための括弧を、 $を使うと取り除くことができることを意味します。

たとえば、ある従業員の上司の住所がある道路を見つける、次の入れ子になった関数適用を考えてみましょう。

street (address (boss employee))

これは $を使用して表現すればずっと簡単になります。

street $ address $ boss employee

3.14 関数合成

イータ変換を使うと insertEntry関数を簡略化できたのと同じように、引数をよく考察すると findEntryの定義を簡略化することができます。

引数 bookが関数 filter filterEntryに渡され、この適用の結果が headに渡されることに注目してください。これは言いかたを変えれば、 filter filterEntryhead合成(composition) に bookは渡されるということです。

PureScriptの関数合成演算子は <<<>>>です。前者は「逆方向の合成」であり、後者は「順方向の合成」です。

いずれかの演算子を使用して findEntryの右辺を書き換えることができます。逆順の合成を使用すると、右辺は次のようになります。

(head <<< filter filterEntry) book

この形式なら最初の定義にイータ変換の技を適用することができ、 findEntryは最終的に次のような形式に到達します。

findEntry firstName lastName = head <<< filter filterEntry
  where
    ...

右辺を次のようにしても同じです。

filter filterEntry >>> head

どちらにしても、これは「 findEntryはフィルタリング関数と head関数の合成である」という findEntry関数のわかりやすい定義を与えます。

どちらの定義のほうがわかりやすいかの判断はお任せしますが、このように関数を部品として捉え、関数はひとつの役目だけをこなし、機能を関数合成で組み立てるというように考えると有用なことがよくあります。

3.15 テスト、テスト、テスト……

これでこのアプリケーションの中核部分が完成しましたので、 PSCiを使って試してみましょう。

$ pulp repl

> import Data.AddressBook

まずは空の住所録から項目を検索してみましょう(これは明らかに空の結果が返ってくることが期待されます)。

> findEntry "John" "Smith" emptyBook

No type class instance was found for

    Data.Show.Show { firstName :: String
                   , lastName :: String
                   , address :: { street :: String
                                , city :: String
                                , state :: String
                                }
                   }

エラーです!でも心配しないでください。これは単に 型 Entryの値を文字列として出力する方法を PSCiが知らないという意味のエラーです。

findEntryの返り値の型は Maybe Entryですが、これは手作業で文字列に変換することができます。

showEntry関数は Entry型の引数を期待していますが、今あるのは Maybe Entry型の値です。この関数は Entry型のオプショナルな値を返すことを忘れないでください。行う必要があるのは、オプショナルな値の中に項目の値が存在すれば showEntry関数を適用し、そうでなければ存在しないという値をそのまま伝播することです。

幸いなことに、Preludeモジュールはこれを行う方法を提供しています。 map演算子は Maybeのような適切な型構築子まで関数を「持ち上げる」ことができます(この本の後半で関手について説明するときに、この関数やそれに類似する他のものについて詳しく見ていきます)。

> import Prelude
> map showEntry (findEntry "John" "Smith" emptyBook)

Nothing

今度はうまくいきました。この返り値 Nothingは、オプショナルな返り値に値が含まれていないことを示しています。期待していたとおりです。

もっと使いやすくするために、 Entryを文字列として出力するような関数を定義し、毎回 showEntryを使わなくてもいいようにすることもできます。

printEntry firstName lastName book
  = map showEntry (findEntry firstName lastName book)

それでは空でない住所録を作成してもう一度試してみましょう。先ほどの項目の例を再利用します。

> book1 = insertEntry entry emptyBook

> printEntry "John" "Smith" book1

Just ("Smith, John: 123 Fake St., Faketown, CA")

今度は結果が正しい値を含んでいました。 book1に別の名前で項目を挿入して、ふたつの名前がある住所録 book2を定義し、それぞれの項目を名前で検索してみてください。

演習

  1. (簡単) findEntry関数の定義の主な部分式の型を書き下し、 findEntry関数についてよく理解しているか試してみましょう。たとえば、 findEntryの定義のなかにある head関数の型は AddressBook -> Maybe Entryと特殊化されています。

  2. (簡単) findEntryの既存のコードを再利用し、与えられた電話番号から Entryを検索する関数を書いてみましょう。また、 PSCiで実装した関数をテストしてみましょう。

  3. (やや難しい) 指定された名前が AddressBookに存在するかどうかを調べて真偽値で返す関数を書いてみましょう。 (ヒント:リストが空かどうかを調べる Data.List.null関数の型を psciで調べてみてみましょう)

  4. (難しい) 姓名が重複している項目を住所録から削除する関数 removeDuplicatesを書いてみましょう。 (ヒント:値どうしの等価性を定義する述語関数に基づいてリストから重複要素を削除する関数 Data.List.nubByの型を、 psciを使用して調べてみましょう)

まとめ

この章では、関数型プログラミングの新しい概念をいくつか導入しました。

次の章からは、これらの考えかたに基づいて進めていきます。

目次に戻る