2009年11月1日日曜日

Haskell の フィールドラベル と デフォルト値をラップする関数

まいど、「名前」と「年齢」を持つ「人」型を例にとってフィールドラベルの使い方を覚える。
フィールドラベルを使わない場合の定義は、

data Person = Person String Int

1. 型を定義するとき

フィールドラベルを使うと、フィールドの意味が明確になる。
data Person = Person { name :: String
                     , age  :: Int
                     } deriving Show

2. 値を生成するとき

値を生成するときにフィールド名を指定できる
jirou = Person { name = "Jirou", age = 20 }

3. 値を取り出すとき

例えば、以下のような「人」の名前を取得するための関数を定義する必要がなくなる。
getName (Person n _) = n
代りに、フィールドラベルでフィールドの値を取得できる。
フィールド名は選択子関数として使用する。変数として使用するときは、 フィールド名はオブジェクトからそのフィールドを取り出す関数として働く。
(3.15.1 フィールドの選択 より、 太字は引用者による)
例えば、
tarou = Person "Tarou" 10
tarouName = name tarou
ただし、次のことに注意。
フィールドラベルは一般の変数やクラスメソッドとトップレベルの名前空間を共有します
(6.2 フィールドラベル より)
同じフィールド名がバッティングした場合、型ごとにモジュールに分けるなど対処が必要。(cf. Haskell のモジュールと型クラス)

4. パターンマッチのとき

例えば、年齢を 2 倍する関数をフィールドラベルを用いず書くとすると、
doubleAge (Person _ a) = a * 2
フィールドラベルを使うことによって、次のように書ける。
doubleAge (Person{ age = a }) = a * 2
つまり、必要なフィールドのみ書けばよい。なぜかこの書き方よく忘れる。。 (+_+)
ただし、この場合は上記の選択子関数を使った方がシンプルに書ける。
doubleAge = (* 2) . age

5. フィールドの更新

次のような年齢を変更する関数を定義する必要がなくなる。
setAge age (Person n _) = Person n age
あたかもフィールドを更新するかのように、
tarou' = tarou { age = 30 }

フィールドの省略について

ところで、フィールドラベルを使って値を生成するとき、特定のフィールドを省略できる。
hanako = Person { name = "Hanako" }
この場合、コンパイルすると警告がでるのみで、エラーとならない。
    Warning: Fields of `Person' not initialised: age
    In the expression: Person {name = "Hanako"}
    In the definition of `hanako': hanako = Person {name = "Hanako"}
Ok, modules loaded: Main.
ただし、「人」型の値から「年齢」を得ようとすると、次のような例外が発生する。
*Main> age hanako
*** Exception … : Missing field in record construction Main.age
つまり、実行時になってやっと判明する。 (+_+)

undefined を使った場合
これに対して、値を生成するとき undefined を使うと、コンパイル時に警告すら出ない。
hanako = Person "Hanako" undefined
しかし、当然ながら「年齢」を得ようとすると、実行時に例外が発生する。
*Main> age hanako'
*** Exception: Prelude.undefined
つまり、変数 hanako’ に上記の doubleAge 関数を適用すると、実行時にエラーとなる。
*Main> doubleAge hanako'
*** Exception: Prelude.undefined

データコンストラクタでデフォルト値を使いたい
例えば、
「名前」は確実にわかるけれど、「年齢」についてはわからない可能性がある
とする。上記のように undefined を使うと実行時エラーになる可能性があるので Haskell の型チェックの機能を十分に生かせない。
[Haskell-beginners] Default values for Data Types? には Maybe a 型と、データコンストラクタへのラッパー関数を作成して対応する方法が書かれていた。これを真似てみる。
まず「人」型の定義から。年齢を Maybe Int 型とする。
type Age = Maybe Int
data Person = Person { name :: String
                     , age  :: Age
                     } deriving Show
上記のデータコンストラクタは次のように使う。
tarou = Person "Tarou" (Just 10)
jirou = Person  "Jirou" Nothing
しかし、このように書くのは面倒なので、データコンストラクタをラップする関数 person を定義。
person = Person { name = "", age = Nothing }
これを使い、値を更新するふりをして、値を定義する。
hanako = person { name = "Hanako" }
フィールドラベルを書くのすら面倒なら、次のようなラッパー関数を定義。
person' n = Person n Nothing
先ほどよりもシンプルに書けるようになった。
akemi = person' "Akemi"

Maybe a 型を使うメリット
上記のように Maybe  a 型を使うといい理由は、Haskell がコンパイル時にエラーで教えてくれるから。
例えば、先ほどと同じように「年齢」を 2 倍にする関数を定義する。年齢がただの Int 型だと思い込んで次のように定義すると、
doubleAge' = (* 2) . age
コンパイル時にエラーが表示される。
    No instance for (Num Age)
      arising from the literal `2'
                   at C:\ …
    Possible fix: add an instance declaration for (Num Age)
    In the second argument of `(*)', namely `2'
    In the first argument of `(.)', namely `(* 2)'
    In the expression: (* 2) . age
Failed, modules loaded: none.
エラーの内容を読むと、Age 型、つまり、Maybe Int 型に (*) を適用するには、Maybe Int 型が Num クラスのインスタンスでないと無理だということ。実行時に突然エラーが表示されるのと違い、コンパイル時に関数適用の間違いを指摘してくれるので、実行する前にコードを修正できる。
例えば、年齢が未定義の場合、 2 倍しても 0 であるとするなら、次のように定義。
doubleAge (Person n a) = case a of
                           Nothing -> 0
                           Just x  -> x * 2

遅延評価させない
ところで、undefined とは、
A special case of error.
error とは、
error stops execution and displays an error message.
では、なぜ値を生成するとき undefined を指定できたのだろう?
6.3 正格データ構築子 によると、
Haskell ではデータ構造は一般的に遅延性をもちます。構成要素は必要になるまで評価されません。この性質のおかげで、データ構造はもし評価されるとエラーあるいは停止できないような要素を含むことができます。
ということは、遅延させなければ、エラーになるのだろうか?
data 宣言の中で、! でマークされたフィールドは、サンクに入れて遅延するのではなく、構造が生成される際に、直ちに評価されます。
(同上より)
上記の「サンク」とは、遅延評価 の説明において、、
遅延評価とは、… 評価しなければならない値が存在するとき、実際の計算を値が必要になるまで行わないことをいう。評価法が指示されているが実際の計算が行われていない中間状態の時それをプロミス (promise) や、計算の実体をさしてサンク (thunk) といい、プロミスを強制(force)することで値が計算される。
(太字は引用者による)
試してみる。下記のようにデータコンストラクトのフィールドの型の前に `!’ をつける。
data Person' = Person' { name' :: !String
                       , age'  :: !Int
                       }
先ほどと同じように一部のフィールドのみ定義すると、
hanako'' = Person' { name' = "Hanako" }
次のようにエラーが表示された。
    Constructor `Person'' does not have the required strict field(s): age'
    In the expression: Person' {name' = "Hanako"}
    In the definition of `hanako''':
        hanako'' = Person' {name' = "Hanako"}
Failed, modules loaded: none.