2009年10月31日土曜日

Haskell の Maybe a 型と Either a b 型 (1) - 「結果を返しそこなう計算」、 Java と比較しながら

091029-011

Bool に並ぶ基本的な型

Prelude の冒頭「Basic data types」には、次の 3 つの型が挙げられている。
Bool 型については、他の言語でも基本的な型としてあるので問題ない。しかし、「Maybe, Either って何だこりゃ?」というのが最初の印象。
何よりも Prelude の最初に記述されているということは重要なんだうろと思ったけれど、例えば次のような説明を読んでも、
… Haskell の Maybe 型についてはよく馴染んでいると思いますが、
data Maybe a = Nothing | Just a

これは結果を返しそこなうかもしれない計算の型を表現しています。
(モナドとは何か より, 太字は引用による)

は? (@_@;) 「結果を返しそこなうかもしれない計算の型」ってどんな型?という疑問が。なぜなら、例えば、自分で定義する 代数的データ型 Person であるなら、それに対応した「人」のイメージを思い浮かべることはできるし、また、真偽値を表わす Bool 型や整数を表わす Int 型も抽象的な概念であるけれど、馴染みがあるので想像の範疇を超えない。それに対して「結果を返しそこなうかもしれない計算」というのは、一体どういうもので、何を具体的に想像すればいいのかわからなかった。

 

Python や Ruby で結果を返しそこなうとは - index

Python の場合
Haskell に出会うまでは、それを一つの概念として認識することはなかった。例えば、Python で Haskell のリストに類似した 変更可能なシーケンス型 には index メソッドが定義されている。(cf. Python の シーケンス型に慣れる)

s.index(x[, i[, j]])
s[k] == x かつ i <= k < j となる最小の k を返します。

`注’ には「xs 中に見つからなかった場合 ValueError を送出します。」と書かれている。
例えば、以下のコードを実行すれば、
ary = [1,2,3]
print ary.index(1)
print ary.index(4)
次のような結果が表示される。
0
Traceback (most recent call last):
  File "index.py", line 3, in 
    print ary.index(4)
ValueError: list.index(x): x not in list

 

Ruby の場合
Ruby でも同様に、Array の index メソッドの説明には、

等しい最初の要素の位置を返します。…
等しい要素がひとつもなかった時には nil を返します。

次のコードを実行。
ary = [1,2,3]
p ary.index(1)   #=> 0
p ary.index(4)   #=> nil

Enumerable の find メソッドも動作は似ている。(cf. Python のイテレータ (2) - Ruby の Enumerable との比較)
上記のようなメソッドは、検索して見つけた場合にはそれに関する情報を返し、見つからなかったら「何もなかったよ」という意味で nil やらエラーを返すという動作をすると理解していた。決して「見つかった場合と見つからなかった場合」を総称する概念が頭の中にあったわけではない。同じメソッドなんだけれど、場合によって返されるものは全く別物と言う感覚。

 

Haskell における具体例 - elemIndex

これに対して Haskell では、上記と同様の関数である Data.List の elemIndex の型を見ると、Mabye Int 型が返される。

elemIndex :: Eq a => a -> [a] -> Maybe Int

次のコードを実行。
import Data.List
main = do let l = [1..3]
          print $ elemIndex 1 l   -- Just 0
          print $ elemIndex 4 l   -- Nothing

Ruby や Python で nil やエラーを返す代わりに、Nothing という値を返していることがわかる。重要なのは Nothing という値が Maybe Int 型であること。つまり「あった場合と、なかった場合」をまとめた概念として Maybe  a 型を想定している。
もし、検索した結果「あった・なかった」だけわかればいいのであれば、Bool 型の True, False の 2 値で代用できる。しかし、Maybe a 型のデータコンストラクタ Just は、elemIndex 関数で言うなら「見つけたインデックス」という具体的な値を`Just という容器’に入れて持ってきてくれる。
さすが型にキッチリカッチリした Haskell らしいやり方と思ったけれど、関数の型を見ればその意図を汲み取れるというのはメリットがある。逆に「見つからなかったとき」どうなるのか、ドキュメントを読まなければわからない方が気持ち悪く思えてきた。 (@_@;) これは Java にしてもそうで、List インターフェイスの indexOf の返す型を見ただけでは、「見つからなかった」ときどうなるのかはわからない。(まぁ、もちろん慣習から想像できるけれど。。)

 

Java の null は忍び込む

静的な型チェックをしてくれる Java だけれど、null に対してはコンパイル時にスルーされる。例えば、「人」が「名前」「住所」の情報を持っており、「人」のリストから「名前」で検索し、「住所」を得たいとする。次のように Java で書いてコンパイルしてもエラーは出ない。
import java.util.Arrays;
import java.util.List;

public class Search {

    private static List<Person> persons = Arrays.asList(
            new Person("Tarou", "Tokyo"),
            new Person("Jirou", "Osaka"),
            new Person("Saburou", "Nagoya"));

    static Person find(String name) {
        for (Person p : Search.persons) {
            if (p.getName().equals(name)) {
                return p;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        System.out.println(find("Tarou").getAddress());
        System.out.println(find("Hanako").getAddress()); // NullPointerException
    }
}

class Person {

    private String name;
    private String address;

    Person(String name, String address) {
        this.name = name;
        this.address = address;
    }

    String getName() {
        return this.name;
    }

    String getAddress() {
        return this.address;
    }

    @Override
    public String toString() {
        return this.name + ":" + this.address;
    }
}
しかし、コードを実行すると NullPointerException が発生する。理由は以前見たように、

null の型である特別な 空型(null type) も存在する。それには名前がない。空型には名前がないので,空型の変数を宣言すること又は空型にキャストすることはできない。空型の式が取りうる唯一の値が空参照となる。 空参照は,常に任意の参照型にキャストできる。
(Java言語規定 型,値及び変数 より)

という仕様による。
つまり、null を返すことは、どこかでキッチリ null のチェックすることが要求され、実行時になって気がつくという危険性が潜むことになる。

 

Haskell はコンパイル時にエラーで教えてくれる

これに対して、Haskell で上記と同じような内容を書こうと思い、間違えて次のように書いたとする。
import Data.List

data Person = Person { name, address :: String} deriving Show

ps = [ Person "Tarou" "Tokyo"
     , Person "Jirou" "Osaka"
     , Person "Saburou" "Nagoya"
     ]

getAddress n = address . find ((== n) . name)

main = putStrLn $ getAddress "Tarou" ps
これを実行しようとしても、その前にエラーが表示される。
    Couldn't match expected type `Person'
           against inferred type `Maybe Person'
    In the second argument of `(.)', namely `find ((== n) . name)'
    In the expression: address . find ((== n) . name)
    In the definition of `getAddress':
        getAddress n = address . find ((== n) . name)
Failed, modules loaded: none.

 

エラーの読み方
いつもちゃんと読もうと思っているのだけれど、おざなりに。 (+_+) 今回はちゃんと読むことにした。 ^^;
とりあえず、下から上へと読むのがわかりやすい。エラーの原因となっている場所が、「定義 → 式 → 引数 」と位置が絞り込まれ、自分が書いた式が返す型に対して、

「この場合だと、本当はこの型が来るんですけどねぇ。。」

とコンパイラが教えてくれる。
 

091030-001


今回は address の使い方を間違えているので、Maybe Person 型を扱えるように変更しなければならない。

 

Maybe a の型に応じた処理を実装
上記の getAddress 関数を以下のように修正。Maybe は Just と Nothing なので、それに応じて処理を分ければよい。
getAddress n ps = case find ((== n) . name) ps of
                    Just x  -> address x
                    Nothing -> n ++ " ha imasen"

main = do putStrLn $ getAddress "Tarou" ps
          putStrLn $ getAddress "Hanako" ps

ちなみに、Haskell だと重複があるとなるべく排除したい気分になるので、main 関数を次のように変えてもよい。
main =  mapM_ (putStrLn . flip getAddress ps) ["Tarou","Hanako"]

コード全体はこちら

 

Java で null を返す代りに例外を投げる

ついでなので、Java ではどのようにしてコンパイル時にエラーを吐いてくれるようにするか考える。
Emulating Haskell's Maybe in Java with Checked Exceptions には例外を使ってコンパイル時にチェックする方法が述べられている。これを真似して上記の Java コードを修正する。
要は、find メソッドにおいて、見つからなかったときに null を返すのはなく、例外を投げるということ。これにより、クライアントが例外をチェックしてなかったら、コンパイラがエラーを表示してくれると。
まずは、Exception クラスを拡張して Nothing を作成。
class Nothing extends Exception {

    Nothing(String name) {
        super(name);
    }
}
Search クラスの find メソッドにおける null を、例外 Nothing で置き換える。
    static Person find(String name) throws Nothing {
        for (Person p : Search.persons) {
            if (p.getName().equals(name)) {
                return p;
            }
        }
        throw new Nothing(name);
    }
main メソッドで例外を捕捉。
    public static void main(String[] args) {
        try {
            System.out.println(find("Tarou").getAddress());
            System.out.println(find("Hanako").getAddress());
        } catch (Nothing e) {
            System.out.println(e.getMessage() + " ha imasen");
        }
    }

これで、try .. catch で捉えないとコンパイル時に怒られるようになった。
コード全体はこちら

 

Java で Null オブジェクトパターンを使う



4894712288
もう一つ、null のチェックをなくしたいと言えば、連想するのはリファクタリングの中で紹介されていた Null Object パターン。(p.260) (cf. Null Object pattern – Wikipedia)
これは、Person クラスを拡張して、検索して見つからなかった場合を表現する NullPerson クラスを作成。ここに null であった場合の振舞を集約する。
class NullPerson extends Person {

    NullPerson(String name) {
        super(name, "");
    }

    @Override
    String getAddress() {
        return super.getName() + " ha imasen";
    }
}
Search クラスの検索メソッドでは、見つからなかった場合、上記の NullPerson クラスのオブジェクトを返す。
    static Person find(String name) {
        for (Person p : Search.persons) {
            if (p.getName().equals(name)) {
                return p;
            }
        }
        return new NullPerson(name);  // null の代わりに Null オブジェクトを返す
    }
これにより、main メソッドで null チェックをしなくても済むようになる。
    public static void main(String[] args) {
        System.out.println(find("Tarou").getAddress());
        System.out.println(find("Hanako").getAddress());
    }

コード全体はこちら

 

Java で Haskell の Maybe a 型を真似る

ついでに、Java の総称型について知識がないので正しい記述なのかわからないが、Haskell の Maybe a 型に相当するクラスを試しに書いてみる。今回は IDE に Netbeans を使っているので、こういうとき IDE が「あれ違うこれ違う」と教えてくれるので、とりあえず動くものが書けるのでありがたい。 ^^
091031-002まずは Maybe a 型に対応したクラスを書く。Java ではパターンマッチによって、コンストラクタで作った中身を取り出せないので、Maybe<T> クラスに fromJust メソッドを実装。これにより Just で包んだ中身の値を取り出す。
class Maybe<T> {
 
    protected T a;
 
    T fromJust() {
        return this.a;
    }
}
 
class Nothing extends Maybe {
}
 
class Just<T> extends Maybe {
 
    Just(T a) {
        this.a = a;
    }
}
Person オブジェクトを検索するメソッドでは、見つかったとき Just クラスのオブジェクトで包み、見つからなかったら Nothing クラスのオブジェクトを返す。
    static Maybe<Person> find(String name) {
        for (Person p : Search.persons) {
            if (p.getName().equals(name)) {
                return new Just(p);    // 見つかったら Just で包む
            }
        }
        return new Nothing();  // Haskell の Maybe a 型の値 Nothing に相当
    }
これにより、find メソッドを使う getAddressメソッドでは、Maybe<Person> で受けなける必要に迫られ、Just と Nothing に応じた処理を書く。
    static String getAddress(String name) {
        Maybe<Person> m = find(name);
        if (m instanceof Just) {
            return m.fromJust().getAddress();
        } else {
            return name + " ha imasen";
        }
    }

コード全体はこちら

 

余談

と、ここまで書いたとき、以前に同じようなことを書いたような気がしたので検索したら、「Haskell の Maybe 型に馴染む」が見つかった。内容については全く記憶から抜けていたにも関わらず、冒頭が今回とほぼ同じだったのにはさすがに笑った。 ^^; そのときはまだ Maybe と Either の類似性には全く気がつかず。Java における null を、どのようにして Haskell では扱うのだろうか?に関心があったようだ。続く「Maybe 型に馴染む (2)」を見ても、モナドについては全くイメージができていなかったことが伺える。やはり、「State モナド (1) - 状態を模倣する」を書いた以前と以後では関数を見る見方が少し違ってきたように思う。

 

関連記事