HOME Haskell のお勉強 download 書き込む

7. 自前の data と class


この文書では自前の data と class を定義する方法について述べます。

1. 最も単純なデータ型

じゃんけんをする簡単なプログラムを書くことを考えます。
そのために、データ型 Jannkenn を下のコードのように定義します。 Jannkenn には Guu, Choki, Paa の3つのデータ (正確には引数を取らない 3 つのデータ生成関数)があります。 データの定義はキーワード data ではじめます。 また、複数の生成関数は '|' で区切ります。 データ名、生成関数名は大文字ではじめます。

data Jannkenn = Guu          
              | Choki        
              | Paa          

2. class について

ついでに、表示の仕方と等しいという関係 (==) を定義しておきましょう。 Haskell ではこれらのことは Show class, Eq class を用いて行います。

Haskell の class はオブジェクト指向で云うところの 総称関数 (generic function)の組にほぼ相当します。 Python の特殊メソッドに相当するといったほうが分かりやすいかもしれません。

show という関数は Show class に属する(総称)関数で、 それのインスタンス(総称関数モデルで云うところのメソッド) をある data 型について定義することによって、 その data を表示する方法が定義されます。
例えば、data Jannkenn における show の instance は次のように定義します。

instance Show Jannkenn where
  show Guu = "Guu"
  show Choki = "Choki"
  show Paa = "Paa"
同様に、(==) は Eq class に属する総称関数で、 data Jannkenn については次のように定義します。
instance Eq Jannkenn where
  Guu == Guu = True
  Paa == Paa = True
  Choki == Choki = True
  _ == _ = False

実はこれはかなり当たり前です。このような当たり前のことは deriving を使うと処理系がやってくれます。
つまり、はじめのデータの定義で、次のようにしておけば、 Show や Eq を自分で定義する必要はありません。

data Jannkenn = Guu          
              | Choki        
              | Paa          
      deriving (Eq, Show)

なお、 付録にじゃんけんをするプログラム jannkenn.hs が付いていますので遊んでみてください。

3. 複合的なデータ型

data Jannkenn はそれ自体で完結したものでした。 この節では、あるデータ型から別のデータ型を作る場合について 述べます。
例として Vector 型を定義してみます。 (コードは下に示します。)

まず、data a からなる data Vector aa のリスト [a]constructor Vec を 作用させて作ります。(1行目)

class Show の instance の作り方が Jannkenn の場合と少し異なっています。 data Vector の表示方法を定義するには、 元になる data a の表示方法が 定義されている必要があるので、6行目のように書きます。 意味は、"data a が class Show の instance であれば、 Vector a は class Show の instance である。" です。ここでは、先頭に 'V' を表示することで Vector を表すようにします。

class Eq の instance も同様にして作ります。

class Num は演算を定義しています。Haskell はデータ型にうるさいので、 演算の前後で型が同じになる必要があります。つまり、内積は 'v1 * v2' のように定義できません。
ここでは、 (+), (-), negate を定義しています。

内積と長さはそれぞれ 15--17, 19--21 行目のように定義します。

01:     data Vector a = Vec [a] 
02:     
03:     instance Show a => Show (Vector a) where
04:         show (Vec xs) = 'V' : show xs
05:     
06:     instance Eq a => Eq (Vector a) where
07:         Vec xs == Vec ys = xs == ys
08:          
09:     --    Num((+), (-), (*), negate, abs, signum, fromInteger),
10:     instance Num a => Num (Vector a) where
11:          Vec xs + Vec ys = Vec $ zipWith (+) xs ys
12:          Vec xs - Vec ys = Vec $ zipWith (-) xs ys
13:          negate (Vec xs) = Vec $ map negate xs
14:     
15:     -- inner product
16:     ipro :: (Num a) => Vector a -> Vector a -> a
17:     ipro (Vec xs) (Vec ys) = sum $ zipWith (*) xs ys
18:     
19:     -- length of vector
20:     vabs :: Vector Double -> Double
21:     vabs (Vec xs) = sqrt $ sum $ map (^2) xs
Mydat> Vec [1,2,3] + Vec [4,5,6]
V[5,7,9]

4. 再帰的なデータ型の定義

data は再帰的に定義することも出来ます。 例えば、List は次のように再帰的に定義されています。
data  [a]  =  [] | a : [a]  deriving (Eq, Ord)
ここでは、お決まりの二分木を定義してみます。 まず、1--3 行目で、data BST (binary state tree) は Eot (End of tree, 終端) か Node (節)だと定義します。 Node は、挿入位置を決める整数のラベル、値、左右の BST からなっています。 BST をそのまま表示すると見にくいので、連想配列にしてから表示します。 (5--7 行目)
以下に関数定義を示します。
関数 説明
10--15 bst_find bst key bst のなかから key をもつレコードを探します。
18--23 bst_insert bst (key, val) bst に (key, val) のレコードを挿入します。
26--28 bst_map f bst 各レコードを f で変換し、全体を連想リストにして返します。
31-32 bst2alist = bst_map id
テストデータ bst1 を 35--36 行目で定義しています。

[code 4.a]

01:     data BST a = Eot
02:                | Node Int a (BST a) (BST a) -- key, val, left, right
03:      deriving Eq
04:     
05:     instance Show a => Show (BST a) where
06:         show Eot = "Null"
07:         show node = "BST" ++ (show $ bst2alist node)
08:     
09:     -- find a record in a BST
10:     bst_find :: BST a -> Int -> Maybe (Int, a)
11:     bst_find Eot _ = Nothing
12:     bst_find (Node key0 val0 left right) key
13:         | key0 == key = Just (key0, val0)
14:         | key0 < key  = bst_find right key
15:         | otherwise   = bst_find left  key
16:     
17:     -- insert a record into a BST
18:     bst_insert :: BST a -> (Int, a) -> BST a
19:     bst_insert Eot (key, val) = Node key val Eot Eot
20:     bst_insert (Node key0 val0 left right) (key, val) 
21:         | key0 == key = Node key0 val left right                             -- if same key value, replace val
22:         | key0 < key  = Node key0 val0 left (bst_insert right (key, val))
23:         | otherwise   = Node key0 val0 (bst_insert left  (key, val)) right
24:     
25:     -- traverse
26:     bst_map :: (a -> b) -> BST a -> [(Int,b)]
27:     bst_map _ Eot = []
28:     bst_map f (Node key val left right) = bst_map f left ++ (key, f val) : bst_map f right
29:     
30:     -- convert to an association list
31:     bst2alist :: BST a -> [(Int, a)]
32:     bst2alist  = bst_map id
33:     
34:     -- test data
35:     bst1 = foldl bst_insert Eot [(5, "five"), (6,"six"), (4,"four"),
36:                                   (3, "three"), (7, "seven"), (1,"One"), (10, "ten") ]

5. 名前付きフィールド

data のフィールドが多くなるとどれが何を意味するのか混乱しやすくなります。 また、1つのフィールドを変更する場合にも変化しない残りのフィールドを 全て列挙する必要があります。

そのような不便を避けるため、名前付きフィールドが用意されています。 4 節の Node には 4 個のフィールドがあるので、名前付きフィールドの例として [code 4.a] を名前付きフィールドを用いて書き直してみました ([code 5.a])。 フィールドの数が増えると効果はもっと顕著でしょう。

[code 5.a]

01:     -- Binary state tree
02:     data BST a = Eot
03:                | Node {
04:                         node_key :: Int,
05:                         node_val ::  a,
06:                         node_left :: BST a,
07:                         node_right :: BST a
08:                       }
09:      deriving Eq
10:     
11:     instance Show a => Show (BST a) where
12:         show Eot = "Null"
13:         show node = "BST" ++ (show $ bst2alist node)
14:     
15:     -- find a record in a BST
16:     bst_find :: BST a -> Int -> Maybe (Int, a)
17:     bst_find Eot _ = Nothing
18:     bst_find node key
19:         | key0 == key = Just (key0, node_val node)
20:         | key0 < key  = bst_find (node_right node) key
21:         | otherwise   = bst_find (node_left node)  key
22:      where key0 = node_key node
23:            
24:     -- insert a record into a BST
25:     bst_insert :: BST a -> (Int, a) -> BST a
26:     bst_insert Eot (key, val) = Node{
27:                                       node_key = key,
28:                                       node_val = val,
29:                                       node_left = Eot,
30:                                       node_right =  Eot
31:                                     }
32:     bst_insert node (key, val) 
33:         | key0 == key = node                                        -- if same key value, replace val
34:         | key0 < key  = node{node_right = bst_insert right0 (key, val)}
35:         | otherwise   = node{node_left = bst_insert left0  (key, val)}
36:      where key0 = node_key node
37:            left0 = node_left node
38:            right0 = node_right node
39:            
40:     -- traverse
41:     bst_map :: (a -> b) -> BST a -> [(Int,b)]
42:     bst_map _ Eot = []
43:     bst_map f node = bst_map f (node_left node) ++
44:                      (node_key node, f (node_val node)) : bst_map f (node_right node)
45:     
46:     -- convert to an association list
47:     bst2alist :: BST a -> [(Int, a)]
48:     bst2alist  = bst_map id
49:     
50:     -- test data
51:     bst1 = foldl bst_insert Eot [(5, "five"), (6,"six"), (4,"four"),
52:                                   (3, "three"), (7, "seven"), (1,"One"), (10, "ten") ]
実行例
Mydat> bst1
BST[(1,"One"),(3,"three"),(4,"four"),(5,"five"),(6,"six"),(7,"seven"),(10,"ten")]
Mydat> bst_insert bst1 (2, "two")
BST[(1,"One"),(2,"two"),(3,"three"),(4,"four"),(5,"five"),(6,"six"),(7,"seven"),(10,"ten")]
Mydat> bst_find bst1 7
Just (7,"seven")
Mydat> bst_find bst1 11
Nothing

6. 自前の class を作る

2 節で述べたように class は総称関数の集合です。

簡単な例として面積を表す class Area を定義してみます。 面積を求めるためには、対象となる図形を定義する必要があります。 ここでは、円、長方形を定義しましょう。

コードは次のようになります。
まず、1--2 行目で class Area を定義します。 この class には area という総称関数があります。
つぎに、円、長方形を定義し、その面積の求め方を定義します。 (4--12 行目)

01:     class Area a where
02:         area :: a -> Double
03:     --
04:     data Circle = Circle Double
05:     
06:     instance Area Circle where
07:         area (Circle r) = pi * r * r
08:     --
09:     data Rectangular = Rectangular Double Double
10:     
11:     instance Area Rectangular where
12:         area (Rectangular w h) = w * h
ちなみに、data aDouble に変えるものなら何でも area として定義することが可能で、 物理的な意味を考慮する必要はありません。 例えば、List の area を以下のように定義することが出来ます。 (なんの役に立つかは知りませんが。。)
instance Area [a] where
    area xs = fromIntegral $ length xs

7. 終わりに

dataclass の定義の方法を簡単に述べてみました。 ここに挙げたコードは hs7.lzh の中にある、jannkenn.hs と mydat.hs にあります。気が向いたらダウンロードして遊んでみてください。

より詳しい情報は Yet Another Haskell Tutorial にあります。