関数とレコード

この章の目標

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

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

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

プロジェクトの準備

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

module Data.AddressBook where

import Prelude

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

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

  • Preludeモジュールには標準的な定義と関数の小さな集合が含まれます。 purescript-preludeライブラリから多くの基礎的なモジュールを再エクスポートしているのです。
  • Control.Plusモジュールにはempty値が定義されています。
  • Data.Listモジュールはlistsパッケージで提供されています。 またこのパッケージはSpagoを使ってインストールできます。 モジュールには連結リストを使うために必要な幾つかの関数が含まれています。
  • Data.Maybeモジュールは、省略可能な値を扱うためのデータ型と関数を定義しています。

訳者注:2つのドット (..) を使用すると、 指定された型構築子の全てのデータ構築子をインポートできます。

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

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

$ cd chapter3
$ spago build

単純な型

JavaScriptの原始型に対応する組み込みデータ型として、PureScriptでは数値型と文字列型、真偽型の3つが定義されています。 これらはPrimモジュールで定義されており、全てのモジュールに暗黙にインポートされます。 それぞれNumberStringBooleanと呼ばれており、PSCiで:typeコマンドを使うと簡単な値の型を表示させて確認できます。

$ spago 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 type Boolean.

最後の例は型検証器によるエラーを示しています。 配列の2つの要素の型を単一化(つまり等価にする意)するのに失敗したのです。

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

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

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

この型が示しているのは指定されたオブジェクトが2つのフィールドを持っているということです。 String型のフィールドnameArray String型のフィールドinterestsです。 後者はつまりStringの配列です。

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

> author.name
"Phil"

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

PureScriptの関数はJavaScriptの関数に対応します。 関数はファイルの最上位で定義でき、等号の前に引数を指定します。

import Prelude -- (+) 演算子をスコープに持ち込みます

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

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

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

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

> add 10 20
30

字下げについての注意

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

宣言が複数行に亙る場合、最初の行以外は最初の行の字下げより深くしなければなりません。

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

add x y z = x +
  y + z

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

add x y z = x +
y + z

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

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

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

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

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

PureScriptの幾つかのキーワードは新たなコードのまとまりを導入します。 その中での宣言はそれより深く字下げされなければなりません。

example x y z =
  let
    foo = x * y
    bar = y * z
  in
    foo + bar

これはコンパイルされません。

example x y z =
  let
    foo = x * y
  bar = y * z
  in
    foo + bar

より多くを学びたければ(あるいは何か問題に遭遇したら)構文のドキュメントを参照してください。

独自の型の定義

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

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

これはEntryという型同義語を定義しています。 型Entryは等号の右辺と等価ということです。 レコードの型はfirstNamelastNamephoneという3つのフィールドからなります。 2つの名前のフィールドは型Stringを持ち、addressは以下で定義された型Addressを持ちます。

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

なお、レコードには他のレコードを含めることができます。

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

type AddressBook = List Entry

なお、List EntryArray Entryとは同じではありません。 後者は項目の配列を表しています。

型構築子と種

List型構築子の一例になっています。 Listそのものは型ではなく、何らかの型 aがあるとき List aが型になっています。 つまり、 List型引数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

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

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

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

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

> :kind Number
Type

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

> :kind List String
Type

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

量化された型

説明しやすくするため、任意の2つの引数を取り最初のものを返す原始的な関数を定義しましょう。

> :paste
… constantlyFirst :: forall a b. a -> b -> a
… constantlyFirst = \a b -> a
… ^D

なお、:typeを使ってconstantlyFirstの型について尋ねた場合、もっと冗長になります。

: type constantlyFirst
forall (a :: Type) (b :: Type). a -> b -> a

型シグネチャには追加で種の情報が含まれます。 abが具体的な型であることが明記されています。

このforallキーワードは、constantlyFirst全称量化された型を持つことを示しています。 つまりabをどの型に置き換えても良く、constantlyFirstはその型で動作するのです。

例えば、aIntbStringと選んだとします。 その場合、constantlyFirstの型を次のように特殊化できます。

Int -> String -> Int

量化された型を特殊化したいということをコードで示す必要はありません。 特殊化は自動的に行われます。 例えば、あたかも既にその型に備わっていたかの如くconstantlyFirstを使えます。

> constantlyFirst 3 "ignored"

3

abにはどんな型でも選べますが、constantlyFirstが返す型は最初の引数の型と同じでなければなりません(両方とも同じaに「紐付く」からです)。

:type constantlyFirst true "ignored"
Boolean

:type constantlyFirst "keep" 3
String

住所録の項目の表示

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

showEntry :: Entry -> String

この型シグネチャが言っているのは、showEntryは引数としてEntryを取りStringを返す関数であるということです。 以下はshowEntryのコードです。

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

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

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

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

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

PSCi対話式モードでは反応を即座に得られるので、素早い試作開発に向いています。 それではこの最初の関数が正しく動作するかをPSCiを使用して確認してみましょう。

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

$ spago build

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

$ spago repl

> import Data.AddressBook

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

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

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

> showAddress address

"123 Fake St., Faketown, CA"

showEntryも、住所の例を含む住所録項目レコードを作って試しましょう。

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

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

住所録の作成

今度は住所録を扱う補助関数を幾つか書いてみましょう。 空の住所録を表す値が必要ですが、これは空のリストです。

emptyBook :: AddressBook
emptyBook = empty

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

insertEntry :: Entry -> AddressBook -> AddressBook

この型シグネチャに書かれているのは、最初の引数としてEntry、第2引数としてAddressBookを取り、新しいAddressBookを返すということです。

既存のAddressBookを直接変更することはしません。 代わりに、同じデータが含まれている新しいAddressBookを返します。 このようにAddressBook不変データ構造の一例となっています。 これはPureScriptにおける重要な考え方です。 変更はコードの副作用であり、効率良く挙動を探る上で妨げになります。 そのため可能な限り純粋関数や不変なデータにする方が好ましいのです。

insertEntryを実装するのにData.ListCons関数が使えます。 この関数の型を見るには、PSCiを起動し :typeコマンドを使います。

$ spago repl

> import Data.List
> :type Cons

forall (a :: Type). 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関数を適用し、結果を作成しています。

カリー化された関数

PureScriptの関数はきっかり1つの引数を取ります。 insertEntry関数は2つの引数を取るように見えますが、カリー化された関数の一例なのです。 PureScriptでは全ての関数はカリー化されたものと見做されます。

カリー化が意味するのは複数の引数を取る関数を1度に1つ取る関数に変換することです。 関数を呼ぶときに1つの引数を渡し、これまた1つの引数を取る別の関数を返し、といったことを全ての引数が渡されるまで続けます。

例えばadd5に渡すと別の関数が得られます。 その関数は整数を取り、5を足し、合計を結果として返します。

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

addFive :: Int -> Int
addFive = add 5

addFive部分適用の結果です。 つまり複数の引数を取る関数に、引数の全個数より少ない数だけ渡すのです。 試してみましょう。

なお、お済みでなければadd関数を定義しなくてはなりません。

> import Prelude
> :paste
… add :: Int -> Int -> Int
… add x y = x + y
… ^D
> :paste
… addFive :: Int -> Int
… addFive = add 5
… ^D

> addFive 1
6

> add 5 1
6

カリー化と部分適用をもっと理解するには、例にあったaddとは別の関数を2、3作ってみてください。 そしてそれができたらinsertEntryに戻りましょう。

insertEntry :: Entry -> AddressBook -> AddressBook

(型シグネチャ中の)->演算子は右結合です。 つまりコンパイラは型を次のように解釈します。

Entry -> (AddressBook -> AddressBook)

insertEntryは単一の引数Entryを取り、新しい関数を返します。 そして今度はその関数が単一の引数AddressBookを取り、新しいAddressBookを返します。

これはつまり、最初の引数だけを与えてinsertEntry部分適用できたりするということです。 PSCiで結果の型が見られます。

> :type insertEntry entry

AddressBook -> AddressBook

期待した通り、戻り値の型は関数になっていました。 この結果の関数に2つ目の引数も適用できます。

> :type (insertEntry entry) emptyBook
AddressBook

ただし、ここでの括弧は不要です。 以下は等価です。

> :type insertEntry entry emptyBook
AddressBook

これは関数適用が左に結合するためで、空白で区切った引数を次々に関数に指定するだけでいい理由もこれで分かります。

関数の型の->演算子は関数の型構築子です。 この演算子は2つの型引数を取ります。 左右の被演算子はそれぞれ関数の引数の型と返り値の型です。

本書では今後、「2引数の関数」というように表現することがあることに注意してください。 しかしそれはカリー化された関数を意味していると考えるべきで、その関数は最初の引数を取り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

この処理はイータ変換と呼ばれ、(その他の技法を併用して)ポイントフリー形式へと関数を書き換えるのに使えます。 つまり、引数を参照せずに関数を定義できるのです。

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

プロパティ取得子

よくあるパターンの1つとして、レコード中の個別のフィールド(または「プロパティ」)を取得することがあります。 EntryからAddressを取り出すインライン関数は次のように書けます。

\entry -> entry.address

PureScriptではプロパティ取得子という略記が使えます。この略記では下線文字は無名関数の引数として振舞うため、上記のインライン関数は次と等価です。

_.address

これは何段階のプロパティでも動くため、Entryに関連する街を取り出す関数は次のように書けます。

_.address.city

以下は一例です。

> address = { street: "123 Fake St.", city: "Faketown", state: "CA" }
> entry = { firstName: "John", lastName: "Smith", address: address }
> _.lastName entry
"Smith"

> _.address.city entry
"Faketown"

住所録に問い合わせる

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

住所録を絞り込めば該当する姓名を持つ項目だけを保持するようにできます。 そうすれば結果のリストの先頭(つまり最初)の要素を返せます。

この大まかな道筋の仕様があれば関数の型を計算できます。 まずPSCiを開いてfilter関数とhead関数の型を探してみましょう。

$ spago repl

> import Data.List
> :type filter

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

> :type head

forall (a :: Type). List a -> Maybe a

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

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

headは引数としてリストを取り、 Maybe aという今までに見たことがない型を返します。 Maybe 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を引数に取り、省略可能な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の型シグネチャを指定しなくても構いません。 ただし、ドキュメントの一形態として指定しておくことが推奨されます。

中置の関数適用

これまでお話しした関数のほとんどは前置関数適用でした。 関数名が引数のに置かれていたということです。 例えばinsertEntry関数を使ってEntry (john) を空のAddressBookに追加する場合、以下のように書けます。

> book1 = insertEntry john emptyBook

しかし本章には中置2引数演算子の例も含まれています。 filterEntryの定義中の==演算子がそうで、2つの引数のに置かれています。 PureScriptのソースコードでこうした中置演算子は隠れた前置の実装への中置別称として定義されています。 例えば==は以下の行により前置のeq関数の中置別称として定義されています。

infix 4 eq as ==

したがってfilterEntry中のentry.firstName == firstNameeq entry.firstName firstNameで置き換えられます。 この節の後のほうで中置演算子を定義する例にもう少し触れます。

前置関数を演算子としての中置の位置に置くと、より読みやすいコードになる場面があります。 その一例がmod関数です。

> mod 8 3
2

上の用例でも充分動作しますが、読みにくいです。 より馴染みのある表現の仕方は「8 mod 3」です。 バックスラッシュ (`) の中に前置関数を包むとそのように書けます。

> 8 `mod` 3
2

同様に、insertEntryをバックスラッシュで包むと中置演算子に変わります。 例えば以下のbook1book2は等価です。

book1 = insertEntry john emptyBook
book2 = john `insertEntry` emptyBook

複数回insertEntryを適用することで複数の項目があるAddressBookを作ることができますが、以下のように前置関数 (book3) として適用するか中置演算子 (book4) として適用するかの2択があります。

book3 = insertEntry john (insertEntry peggy (insertEntry ned emptyBook))
book4 = john `insertEntry` (peggy `insertEntry` (ned `insertEntry` emptyBook))

insertEntryには中置演算子別称(または同義語)も定義できます。 この演算子の名前に適当に++を選び、優先度5にし、そしてinfixrを使って右結合とします。

infixr 5 insertEntry as ++

この新しい演算子で上のbook4の例を次のように書き直せます。

book5 = john ++ (peggy ++ (ned ++ emptyBook))

新しい++演算子の右結合性により、意味を変えずに括弧を除去できます。

book6 = john ++ peggy ++ ned ++ emptyBook

括弧を消去する他のよくある技法は、いつもの前置関数と一緒にapplyの中置演算子$を使うというものです。

例えば前のbook3の例は以下のように書き直せます。

book7 = insertEntry john $ insertEntry peggy $ insertEntry ned emptyBook

括弧を$で置き換えるのは大抵入力しやすくなりますし(議論の余地がありますが)読みやすくなります。 この記号の意味を覚えるための記憶術として、ドル記号を2つの括弧に打ち消し線が引かれたものと見ることで、これで括弧が不必要になったのだと推測できるという方法があります。

なお、($)は言語にハードコードされた特別な構文ではありません。 単にapplyという名前の普通の関数のための中置演算子であって、Data.Functionで以下のように定義されています。

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

infixr 0 apply as $

apply関数は、他の関数(型は(a -> b))を最初の引数に、値(型はa)を2つ目の引数に取って、その値に対して関数を呼びます。 この関数が何ら意味のあることをしていないようだと思ったら、全くもって正しいです。 この関数がなくてもプログラムは論理的に同一です(参照透過性も見てください)。 この関数の構文的な利便性はその中置演算子に割り当てられた特別な性質からきています。 $は右結合 (infixr) で低い優先度 (0) の演算子ですが、これにより深い入れ子になった適用から括弧の束を削除できるのです。

さらなる$演算子を使った括弧退治のチャンスは以前のfindEntry関数にあります。

findEntry firstName lastName book = head $ filter filterEntry book

この行をより簡潔に書き換える方法を次節の「関数合成」で見ていきます。

名前の短い中置演算子を前置関数として使いたければ括弧で囲むことができます。

> 8 + 3
11

> (+) 8 3
11

その代わりの手段として演算子は部分適用でき、これには式を括弧で囲んで演算子節中の引数として_を使います。これは簡単な無名関数を作るより便利な方法として考えることができます(以下の例ではそこから無名関数を名前に束縛しているので、もはや別に無名とも言えなくなっていますが)。

> add3 = (3 + _)
> add3 2
5

纏めると、以下は引数に5を加える関数の等価な定義です。

add5 x = 5 + x
add5 x = add 5 x
add5 x = (+) 5 x
add5 x = 5 `add` x
add5   = add 5
add5   = \x -> 5 + x
add5   = (5 + _)
add5 x = 5 `(+)` x  -- よおポチ、中置に目がないっていうから、中置の中に中置を入れといたぜ

関数合成

イータ変換を使うと 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関数のわかりやすい定義を与えます。

どちらの定義のほうが分かりやすいかの判断はお任せしますが、このように関数を部品として捉えるとしばしば有用です。 各関数は1つの役目をこなすようにし、解法を関数合成を使って組み立てるのです。

演習

  1. (簡単)findEntry関数の定義の主な部分式の型を書き下し、 findEntry関数についてよく理解しているか試してみましょう。 例えばfindEntryの定義の中にある head関数の型は AddressBook -> Maybe Entryと特殊化されています。 補足:この問題にはテストがありません。
  2. (普通)関数findEntryByStreet :: String -> AddressBook -> Maybe Entryを書いてください。 この関数は与えられた通りの住所からEntryを見付け出します。 手掛かりfindEntryにある既存のコードを再利用してください。 実装した関数をPSCiとspago testを走らせてテストしてください。
  3. (普通)filterEntryを(<<<>>>を使った)合成で置き換えて、findEntryByStreetを書き直してください。 合成の対象は、プロパティ取得子(_.記法を使います)と、与えられた文字列引数が与えられた通りの住所に等しいかを判定する関数です。
  4. (普通)名前がAddressBookに存在するかどうかを調べて真偽値で返す関数isInBookを書いてみましょう。 手掛かり:PSCiを使ってData.List.null関数の型を見付けてください。 この関数はリストが空かどうかを調べます。
  5. (難しい)「重複」している住所録の項目を削除する関数removeDuplicatesを書いてみましょう。 項目が同じ姓名を共有していればaddressフィールドに関係なく、項目が重複していると考えます。 手掛かりData.List.nubByEq関数の型をPSCiを使って調べましょう。 この関数は等価性の述語に基づいてリストから重複要素を削除します。 なお、それぞれの重複する項目の集合において最初の要素(リストの先頭に最も近い)が保持する項目です。

まとめ

この章では関数型プログラミングの新しい概念を幾つか押さえ、以下の方法を学びました。

  • 対話的モードのPSCiを使用して、関数で実験したり思いついたことを試したりする。
  • 正確さのための道具として、また実装のための道具として型を使う。
  • 多引数の関数を表現するためにカリー化された関数を使う。
  • 合成により小さな部品からプログラムを作る。
  • where式を使ってコードを手際良く構造化する。
  • Maybe型を使用してnull値を回避する。
  • イータ変換や関数合成のような技法を使ってより分かりやすい仕様にリファクタする。

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