2008年11月5日水曜日

Haskell の Maybe 型に馴染む

モナドの説明を読みはじめると最初の方に必ず出てくる Maybe 型。モナド自体いつか理解したいと思いつつ、解説されているサイトを読んでも途中で挫折。(o_ _)o~† ここは一つ外堀から埋めていこうと、 Monad クラスのインスタンスである Maybe 型から見ていくことに。

 

Prelude における位置付け

Maybe 型は、Prelude の説明の最初、「Basic data types 」の中に含まれていることから考えて、相当重要な型のようだ。Basic data types に含まれている他の型を挙げると、

Either a b 型って何だろうなと横目で見つつ、他の型を見ると基本的で重要なものばかり。(@_@) ちなみに、Haskell Hierarchical Libraries には Data.Maybe とある。

All About Monads」 を読んでも、冒頭の Introduction から Maybe について書かれている。

これは結果を返しそこなうかもしれない計算の型を表現しています。 Maybe 型が示唆するものは、Maybe 型の値を返す計算の合成に関する戦略です。

何やら難しい表現が… (@_@;)

 

Java の null

これが例えば Java なら、次のような場合を考えてみる。例えばここに「人」を表わす Person クラスがあるとする。属性に名前を持つ。Person クラスの配列から `名前’ を元に Person オブジェクトを検索するメソッド (Search.find) を考えると、

public class Search {
    private static Person[] persons = {new Person("Tarou"), 
                                       new Person("Hanako"),
                                       new Person("Jiro")};
    public static void main(String[] args) {
        System.out.println(find("Hanako"));
        System.out.println(find("Saburou"));
    }
    static Person find(String name){
        for(Person p : persons){
            if (p.getName().equals(name)){
                return p;
            }
        }
        return null;
    }
}
class Person{
    private String name;
    Person(String name){
        this.name = name;
    }
    String getName(){
        return this.name;
    }
    @Override
    public String toString(){
        return this.name;
    }
}

“Hanako” を検索した場合は、Person(“Hanako”) を返すことができる。しかし、”Saburou” を検索しても見つけることができない。Search.find メソッドは Person クラスのオブジェクトを返すが、Java において、参照型をメソッドの返り値にすると null も返すことができる。

null の型である特別な 空型(null type) も存在する。それには名前がない。空型には名前がないので,空型の変数を宣言すること又は空型にキャストすることはできない。空型の式が取りうる唯一の値が空参照となる。 空参照は,常に任意の参照型にキャストできる。実際には,Javaプログラマは,空型を無視し,空型は任意の参照型となれる単なる特別なリテラルであると見なしても良い。

(Java言語規定 型,値及び変数 より。太字は引用者による)

検索しても見つからなかったら null を返す。これが自分にとって、メソッドにおける普通の感覚だった。

 

Haskell で検索メソッドを実装

Haskell でも上記の Java と同じような内容のコードを書いてみる。Data.Listfind 関数があるが、これは使わずに横に置いておく。

最初、次のように書いてみた。

data Person = Person {name :: String} deriving (Show,Eq)

persons = [Person "Tarou", Person "Hanako", Person "Jiro"]

find1 :: String -> [Person] -> Person
find1 n (p:ps) = if (name p) == n then p else find1 n ps

main = do
  print $ find1 "Hanako" persons
  print $ find1 "Saburou" persons

結果は “Saburou” の検索で、

Non-exhaustive patterns in function find1

 

ここで当初、null とか nil とか None のようなものを返すことはできないかな?と思った。しかし、リストの要素が空であるか確かめる null 関数は見つかったが、探しものは見つからず。

さすがに、find1 で次のようにするわけにもいかないし。

find1 n [] = Person("null")

これでは 名前が「null」さんを返すことになってしまう。^^; 「見つからなかった」と意味が全然違う。

 

では、Person 型に Empty というデータコンストラクタを定義し、

data Person = Person {name :: String} 
            | Empty 
              deriving (Show,Eq)

find1 に以下を追加したらどうだろう?

find1 n [] = Empty

しかし、これも名前のない「空の人」という意味として使ったとして、何だかしっくりこない感じが。「見つからなかった」ということに使うにしても、それが Person 型の値になるので違和感もある。

 

Maybe

と、そのとき、ふつうのHaskellプログラミング がモナドの章に到達し、何がなんだかわからないうちに Maybe モナドが出現。(@_@;) lookup 関数を使って Monad と Maybe の関係が説明されていたが、このときモナドだけで目が回っていたので、当然ながらその関係も把握できず。ただし、これを使えば上記の null に相当するものに使えそうなことだけはわかった。Prelude の説明を読むと、

Using Maybe is a good way to deal with errors or exceptional cases without resorting to drastic measures such as error.

データコンストラクタは、Nothing と Just a。

 

Monad についてよくわからないけれど、とにかく Maybe を使ってみることに。検索して見つかったら Just で包み、見つからなかったら Nothing を返す。

find2 :: String -> [Person] -> Maybe Person
find2 n [] = Nothing
find2 n (p:ps) = if (name p) == n then Just p else find2 n ps

この関数を使うと、

  print $ find2 "Hanako" persons     # Just (Person {name = "Hanako"})
  print $ find2 "Saburou" persons    # Nothing

 

ただし、上記の方法だと、検索が Person 型に固定されてしまっている。Person 型から 型変数 a に変えるなら、

find3 :: a -> [a] -> Maybe a
find3 t [] = Nothing
find3 t (x:xs) = if x == t then Just t else find3 t xs

しかし、これだとエラーが。 (+_+)

    Could not deduce (Eq a) from the context ()
      arising from a use of `==' at searchtest.hs:20:20-25
    Possible fix:
      add (Eq a) to the context of the type signature for `find3'
    In the predicate expression: x == t

なるほど、x == t と演算子を適用できるには、型変数に対応した値が、 Eq クラスのインスタンスである型の値である必要があるということ。だから、関数の型宣言を以下のように変更。

find3 :: Eq a => a -> [a] -> Maybe a

Person 型は Eq クラスのインスタンスなので、find3 関数を適用することができる。

 

しかし、上記の find3 関数は Eq クラスの (==) メソッドの実装に依存し、検索方法が固定されてしまっている。そこで、「検索の基準」を与えることによって、探したい値を得ることができるように変更。

find4 :: (a -> Bool) -> [a] -> Maybe a
find4 f [] = Nothing
find4 f (x:xs) = if f x then Just x else find4 f xs

この関数を使うには、

  print $ find4 (\p -> (name p) == "Hanako") persons
  print $ find4 (\p -> (name p) == "Saburou") persons

 

Data.List の find

これで Data.List の find 関数の見かけと同じようになった。

find :: (a -> Bool) -> [a] -> Maybe a

Data.List の find の定義を見たら、filter 関数が使われていた。

find                    :: (a -> Bool) -> [a] -> Maybe a
find p                  =  listToMaybe . filter p
(The Haskell 98 Library Report: List Utilities より)

あぁ~、「検索」と「フィルタ」は同じことか。 (@_@)

listToMaybe  関数は、リストを与えると、その先頭要素を Just で包んで返してくれる。

listToMaybe            :: [a] -> Maybe a
listToMaybe []         =  Nothing
listToMaybe (a:_)      =  Just a
(The Haskell 98 Library Report: Maybe Utilities より)

関連記事