Haskellを学んでいると,Maybeという型に遭遇します。

 Maybeとは一体何でしょうか? なぜほかの型ではなくMaybeを使用するのでしょうか?

 読者のなかにはこのような疑問を持った方もいれば,持たなかった方もいるでしょう。あるいは部分的な答えに「Haskellではこうするのだ」と納得して,そこで問いをやめてしまったかもしれません。いずれにせよ,何かを得ることを目的にHaskellを学んでいるのなら,今一度,何を知らないかを自覚し,疑問を思い起こすべきです。

 与えられた情報をただ漫然と常識として受け止めてしまうと,答えとなるような知識を得ることはできません。答えを得るためには,問いが必要なのです。

 今回はMaybe型の説明を通じて,そうした問いへの入り口を示したいと思います。

前回の補足

 前回GHC6.6で使用する文字コードとして紹介したUTF-8Nは,標準化の議論が一時期されていましたが,結局採用されなかったようです(参考リンク1参考リンク2)。現在,BOMなしのUTF-8は存在しても,UTF-8Nという文字コードそのものは存在しません。気をつけましょう。

 GHC6.6を使用するときに文字コードについて気をつけなければならない点を以下にまとめます。

  1. GHC6.6で使用する文字コードはUTF-8
  2. ただし,GHC6.6ではBOMつきのUTF-8は使えない
  3. エディタによっては,文字コードをUTF-8に設定するとBOM付きで保存されることがあるので
    1. その場合には,BOMなしのUTF-8を指定する必要がある
    2. 国産のエディタには,UTF-8NをBOMなしのUTF-8を指すものとして使っているものが多く存在する。この場合UTF-8Nを指定する必要があるが,UTF-8Nという文字コードは実際には存在しないことに留意する

 ややこしいですね。次期標準であるHaskell'では「少なくともUTF-8とUTF-16両方をサポートしなければならない」とすることが提案されています。いずれこうした区別に悩まされなくても済むようになる日が来ると思います(参考リンク)。

Maybe型

 第2回でも少し触れましたが,まずはMaybeの定義を見てみましょう。「Haskell 98 言語とライブラリ 改訂レポート」では,Maybeは以下のように定義されています(参考リンク)。

data  Maybe a  =  Nothing | Just a      deriving (Eq, Ord, Read, Show)

 Maybeは,無引数のデータ構成子Nothing,またはデータ構成子Justと型変数aで構成されているのがわかります。これらのデータ構成子は,何か値があればJustに値を包み,何も値がなければNothingを使用する,というように使い分けられます。

 前回の第4回で紹介したListと同様に,Maybeの定義の中にも直接触れることのできないものはありません。

Prelude> Just 12
Just 12
Prelude> Nothing
Nothing

 次に,Maybeにおけるmapの定義を見てみましょう。標準Haskellでは,Maybeのfmapは以下のように定義されています(参考リンク)。

instance  Functor Maybe  where
    fmap f Nothing    =  Nothing
    fmap f (Just x)   =  Just (f x)

 ちなみに,Data.Maybe(またはMaybe)にはmapMaybeという関数がありますが,これはMaybeに対するmapではないことに注意してください。mapMaybeは,関数を適用した結果がJustになるものだけを集めたリストを返します。つまり,ListではなくMaybeを返す関数に対するconcatMapに相当します。

Prelude Data.Maybe> :t mapMaybe
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
Prelude Data.Maybe> mapMaybe (\x -> if x <= 1 then Nothing else Just x) [0..5]
[2,3,4,5]

 さて,ここまで見てきて,Maybeの定義がListにそっくりであることに気付いた人がいるかもしれません。これは,MaybeのNothingを[ ]で,Justを[x]で代替できることを意味しています。

 MaybeがListで代替可能であるのなら,あえてListではなくMaybeを使う意義はどこにあるのでしょうか?

 一つはプログラムの明確化です。ListではなくMaybeを使うことで,「値がないか,一つ以上の値を持つかのどちらか」ではなく,「値がないか,値があるかのどちらか」であることが明確になります。

 また,Maybeという型を別途導入することにより,そうした意図を静的な型チェックによって保証できるようになります。Listを使った場合,間違って二つ以上の値を持たせてしまう可能性があります。「依存型(dependent type)」を持たない現在の標準Haskellでは,このような状況が不正であることを静的に調べることはできません。このため,Listを使用する場合には,error関数などを使い,不正な状況に対して実行時エラーを発する(動的な型チェックを行う)必要が出てきます。

 実行時エラーの例を見てみましょう。リストの先頭を返す関数headや,先頭を取り除いたリストを返す関数tailは,対象となるリストが空リストであるとき実行時エラーを生じさせます(headとtailは,それぞれLispのcarとcdrに対応します)。

Prelude Data.Maybe> head []
*** Exception: Prelude.head: empty list
Prelude Data.Maybe> tail []
*** Exception: Prelude.tail: empty list

 実行時エラーは,「静的型付け(static typing)」の行き届かないところを補うという意味では有用なものです。ですが,実行時エラーに頼っていては,Haskellの静的型言語としての利点を十分に生かすことはできません。Haskellは「いったんコンパイルさえ通ればそのプログラムにはまずバグはない」(参考リンク)とよく言われます。しかし,実行時エラーでチェックしなければならないような状況では,これは当てはまりません。

 このような問題を防ぐには,何でもかんでもListを使用するのではなく,目的に応じた適切な型を使用する必要があります。静的な型チェックはバグを防ぐのに有用ですが,万能ではありません。静的型の利点を生かすには,目的に応じた適切な型を使用することが重要なのです。

 このように,Maybeのような新しい型を別途導入することには利点があることがわかりました。しかし多くの物事がそうであるように,利点があれば欠点もあります。新しい型を導入した場合,新しい型を前の型と同じものであるかのように扱うには,前の型が持っていたのと同等の関数を定義してやる必要があります。例えばFunctorやShow,等価性(==)メソッドを提供するEqなどの型クラスに対し,「インスタンス宣言(instance declaration)」を行う必要があります。

 ただ,関数によっては「型を定義した人がよく使うであろうデフォルトの定義」の見当が付くことがあります。このため,Haskellには型の宣言時に自動的にインスタンス宣言を生成するための仕組みが用意されています。型宣言に「deriving節(deriving clause)」を使い「導出インスタンス宣言(derived instance declaration)」を加えることで,デフォルトの定義に基づいたインスタンスの定義が自動生成されます。

 ちなみに,型宣言と独立して導出インスタンス宣言ができると,ライブラリですでに提供されている型に対してもインスタンス宣言の自動生成が行えるので,より便利です。このためGHCの開発版(GHC HEAD)には,実験的に「独立型(スタンドアローンの)導出インスタンス宣言(Stand-alone deriving declarations)」(参考リンク)という機能が実装されています。

 では,Maybeの定義をもう一度示します。標準HaskellではMaybeはEq,Ord,ReadおよびShowの四つのクラスのインスタンスを自動的に生成するよう定義されているのがわかるでしょう。

data  Maybe a  =  Nothing | Just a      deriving (Eq, Ord, Read, Show)

 導出インスタンス宣言は新しい型を導入するコストを下げますが,使える範囲が限られていることに注意してください。現在の標準Haskellで導出インスタンス宣言の対象にできるのは,PreludeのEq,Ord,Enum,Bounded,Show,Read,およびData.Ix(またはIx)モジュールのIxの七つのみです(参考リンク1参考リンク2)。なお,GHCにはTypeableやDataといった型クラスも導出インスタンスの対象にするための拡張が加えられています(参考リンク)。

 導出インスタンス宣言の対象外のクラスは自分の手で定義する必要があります。また,少しでもメソッドの定義を変えたい場合には導出インスタンス宣言を使用することはできません。少なくともそのメソッドに対しては,独自の完全な定義を与えてやる必要があります。

 導出インスタンス宣言には多くの制限があることから,ほかにも導出インスタンスを実現するための方法がいくつか提案されています。TypeableクラスやDataクラスを利用するScrap your boilerplate(SYB)などの「総称的プログラミング(generic programming)」を使ったアプローチは,その一つです(参考リンク)。こうした方法を利用するには標準Haskell外の機能が必要になるうえ,説明しなければならない事項も多いことから,ここでは深くは説明しません。今は,導出インスタンスを実現するための方法がほかにもあることがわかれば十分です。興味のある方は自分で調べてみてください。いつかは,この連載でもそれらの方法について解説できる日が来ればと思います。