あどけない話

インターネットに関する技術的な話など

Haskell vs OOP

Why Haskell matters?」(なぜ Haskell は重要か?)には、Haskellオブジェクト指向プログラミングを比較した章があります。日本語訳が見当たらなかったので、必要な部分を訳してみます。

オブジェクト指向プログラミングの優れた利点は、データとそれに作用する関数を一つのオブジェクトにまとめられることではない。優れた利点、それは、(実装からインターフェイスを切り離せる)データのカプセル化と、(型の一群の振る舞いを同じようにする)多相性だ。しかしながら、データのカプセル化と多相性は、OOP の専売特許ではない!

いやぁ、心洗われる文章です。:-)

データのカプセル化

Haskell でのデータのカプセル化は、それぞれのデータ型をそれぞれのモジュールで宣言し、そのモジュールからインターフェイスだけを公開することで実現できる。モジュール内部には、内部データに触れる関数群があるが、モジュールの外から見えるのはインターフェイスだけだ。

たとえば、Stack を定義してみましょう。

module Stack (Stack, push, pop, top, empty) where

data Stack a = Empty | Push a (Stack a) deriving Show

empty = Empty

push :: a -> Stack a -> Stack a
push x s = Push x s

pop :: Stack a -> Stack a
pop Empty = error "pop: empty stack"
pop (Push x s) = s

top :: Stack a -> a
top Empty = error "top: empty stack"
top (Push x s) = x

注目して欲しいのは、module 宣言の部分です。ここで、

module Stack (Stack(Empty,Push), push, pop, top, empty) where

ではなく、

module Stack (Stack(..), push, pop, top, empty) where

でもなく、

module Stack (Stack, push, pop, top, empty) where

のようになっていることが重要です。

このおかげで、Stack モジュールを import するプログラムでは、データ構成子 Empty や Push を利用できません。そのため、Stack 型の内部を触れないのです。(deriving Show としているので、表示はできますよ!)

Stack は、次のようにして使います。

top $ push 2 $ push 1 empty → 2

データ構成子は、他のモジュールから利用されてないことが保証できるので、実装を自由に変更できます。たとえば、今度はリストを使ってみましょう。

module Stack (Stack, push, pop, top, empty) where

data Stack a = ArrayStack [a] deriving Show

empty = ArrayStack []

push :: a -> Stack a -> Stack a
push x (ArrayStack xs) = ArrayStack (x:xs)

pop :: Stack a -> Stack a
pop (ArrayStack []) = error "pop: empty stack"
pop (ArrayStack (x:xs))  = ArrayStack xs

top :: Stack a -> a
top (ArrayStack []) = error "top: empty stack"
top (ArrayStack (x:xs))  = x

当然ですが、以下のコードは、ちゃんと動きます。

top $ push 2 $ push 1 empty → 2

多相性

多相性は、型クラスと呼ばれるもので実現できる。
...
Haskell はクラスのインスタンス化とデータ型の宣言を分離している。たとえば、型クラス Car のインスタンスとして、データ型 "Porsche" を宣言しよう。型クラス Car の他のすべてのメンバーで使える関数のすべてが、Porshe にも適応できる。

これは、よく分りませんでした。たとえば、オブジェクト指向の教科書によく出てくる図形を定義してみます。

class Shape a where
    display :: a -> String

data Triangle = Triangle deriving Show
instance Shape Triangle where
    display _ = "  *  \n" ++
                " * * \n" ++
                "*****\n"

data Rectangle = Rectangle deriving Show
instance Shape Rectangle where
    display _ = "*****\n" ++
                "*   *\n" ++
                "*****\n"

Shape は、型クラスであって、型ではありません。ですから、Triangle と Rectangle を同じ型だと思って、リストにまとめることができません。

[Triangle, Rectangle] :: [Shape] -- これはエラー

まったく分らないので、著者の Sebastian Sylvan さんにメールで質問したら、さくっと答えが返ってきました。

ExistentialQuantification という拡張を使うとできるのだそうです。こんな感じです。

{-# LANGUAGE ExistentialQuantification #-}

class Shape a where
    display :: a -> String

data Triangle = Triangle deriving Show
instance Shape Triangle where
    display _ = "  *  \n" ++
                " * * \n" ++
                "*****\n"

data Rectangle = Rectangle deriving Show
instance Shape Rectangle where
    display _ = "*****\n" ++
                "*   *\n" ++
                "*****\n"

data Polygon = forall a . (Shape a, Show a) => Polygon a

instance Shape Polygon where
    display (Polygon x) = display x

instance Show Polygon where
    show (Polygon a) = "Polygon " ++ show a

ここでは、Polygon という存在量化されたデータ構築子を使っています。

これで、こんなことができます。

[Polygon Triangle, Polygon Rectangle]
→ [Polygon Triangle,Polygon Rectangle]
mapM_ putStr $ map display [Polygon Triangle, Polygon Rectangle]
  *  
 * * 
*****
*****
*   *
*****

もうちょっと簡潔に書きたい気もしますが、まぁ、これでよしとしましょう。