LoginSignup
2
1

More than 3 years have passed since last update.

「Learn You a Haskell」の「Making Our Own Types and Typeclasses」の写経 その壱

Posted at

Haskellで独自のデータ型、型クラスを定義する方法を知りたかったので Learn You a Haskell for Great Good!Making Our Own Types and Typeclasses を写経してます。

今回は 「Algebraic data types intro」 です。


独自のデータ型を作る方法のひとつは型の定義に data キーワードを使うことです。標準ライブラリの Bool 型の定義は以下のようになっています。

data Bool = False | Ture

data は新しい型の定義を意味します。= の手前、ここでは Boolであることを示しています。= の後ろの部分は 値コンストラクタ です。定義した型が取りうる異なる値を指定しています。

|or と認識できます。なのでここでは BoolTrue もしくは False を持つことができるのだと読み取ることができます。型と値コンストラクタはどちらも大文字である必要があります。

似たような例として Int は以下のような定義になっていると考えることができます。

data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647

最初と最後の値コンストラクタは最小・最大値です。
※これは説明のための例であって、実際にはこのように宣言されていません。

さて、 Haskell で図形を表す方法を考えてみましょう。1つの手段は Tuple を使うことです。
円は (43.1, 55.0, 10.0) と表すことができ、1つめと2つめのフィールドは円の中心の座標を示しており、3つ目は半径を示しています。

他にも三次元を表現することも可能です。よりよいソリューションは独自の型を作成して図形を表現することです。円もしくは長方形を表現した型は以下のようになります。

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

上記について考えて見ましょう。
Circle の値コンストラクタは3つの Float のフィールドを持ちます。値コンストラクタを書くとき、必要に応じていくつかの型を追加でき、それらの型はそれに含まれる値を定義します。

Rectangle の値コンストラクタは4つのフィールドを持ち、最初の2つは左上の座標、残りの2つは右上の座標を示しています。

値コンストラクタは実際には最終的にデータ型の値を返す関数です。これらの値コンストラクタの型シグネチャは以下のようになっています。

ghci>:t Circle
Circle :: Float -> Float -> Float -> Shape
ghci>:t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape

図形を引数とし、面積を計算して返す関数を作ってみます。

surface :: Shape -> Float
surface (Circle _ _ r) = pi * r ^ 2
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)

注目すべきは型の宣言です。Shape を引数に取り、Float を返すことを示しています。Circle は型ではありませんので、Circle -> Float というような型定義はできません。True -> Int のような型定義が作れないのと同様です。

次に注目すべきはコンストラクタに対してパターンマッチしているところです。[]False, 5 のような値に対してもパターンマッチできますがこれらはいくつかフィールドを持っているわけではありません。ここではコンストラクタを書いて、値に名前を付けてバインドしています。Circle に関しては値に関心があるので1つ目と2つ目のフィールドに関しては気にしていません。

ghci>surface $ Circle 10 20 10
314.15927
ghci>surface $ Re
ghci>surface $ Rectangle 0 0 100 100
10000.0

作成した関数が動作することを確認できました。

しかし、Circle 10 20 5 をプロンプトに打ち込むとエラーになってしまいます。これは Haskell が私達のデータ型を文字列として表示するする方法を知らないからです。

プロンプトに値を表示させようとすると Haskell は値の文字列表現を取得するために show 関数を実行します。そのため Shape 型を Show 型クラスの一部として作成する必要があります。次のように修正します。

data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

deriving(Show) をデータ宣言の末尾に追加すると、Haskell は自動的にその型を Show 型クラスの実装を自動導出します。

ghci>Circle 10 20 5
Circle 10.0 20.0 5.0
ghci>Rectangle 50 230 60 90
Rectangle 50.0 230.0 60.0 90.0

値コンストラクタは関数なので、それらをマッピングして部分適用するような使い方もできます。

ghci>map (Circle 10 20) [4,5,6,6]
[Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]

これらのデータ型は更に良くすることができます。二次元空間の座標を定義する中間データ型を作成します。

data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)

Point を定義するときに、データ型と値コンストラクタに同じ名前を使用していることに注目してください。これに特別な意味はありませんが、値コンストラクタが1つしかない場合は、型と同じ名前を使用するのが慣習となっています。

さて、これで CirclePointFloat の2つのフィールドを持つようになりました。各フィールドが何を意味するのかより理解しやすくなりました。Rectangle においても同様です。surface 関数にもこれらの変更を反映させます。

surface :: Shape -> Float
surface (Circle _ r) = pi * r ^ 2
surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)

変更が必要だったのはパターンマッチしている箇所のみです。Circle のパターンマッチでは Point のフィールドに該当する箇所を無視しています。Rectangle では Point を階層化してパターンマッチしています。Point そのものを参照したい場合には、as-patterns を利用できます。

ghci>surface (Rectangle (Point 0 0) (Point 100 100))
10000.0
ghci>surface (Circle (Point 0 0) 24)
1809.5574

さて、これら図形の位置を調節する関数を作る場合はどのようになるのでしょうか。図形と、x軸とy軸の移動距離を引数にとり。同じ寸法の新しい形状の図形を返却するようにすればよさそうです。単に別の箇所に位置しているということになります。

nudge :: Shape -> Float -> Float -> Shape
nudge (Circle (Point x y) r) a b = Circle (Point (x + a) (y + b)) r
nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1 + a) (y1 + b)) (Point (x2 + a) (y2 + b))

非常に簡単です。図形の位置を示す Point のフィールドに調整する値を加算するだけです。

ghci>nudge (Circle (Point 34 34) 10) 5 10
Circle (Point 39.0 44.0) 10.0

もし、Point を直接扱いたくなければ、ゼロ座標で図形を生成する補助関数を作って利用することができます。

baseCircle :: Float -> Shape
baseCircle r = Circle (Point 0 0) r

baseRect :: Float -> Float -> Shape
baseRect width height = Rectangle (Point 0 0) (Point width height)
ghci>nudge (baseRect 40 100) 60 23
Rectangle (Point 60.0 23.0) (Point 100.0 123.0)

当然ですが、独自のデータ型を独自のモジュールとして export することもできます。そのためにはまず、export する関数と共に型を記述します。値コンストラクタの export は括弧を追加して、その中でカンマで区切って値コンストラクタを指定します。すべての値コンストラクタを export したい場合は括弧内を .. とします。

これまでに定義したデータ型と関数を export するには以下のようにします。

module Shapes   
( Point(..)  
, Shape(..)  
, surface  
, nudge  
, baseCircle  
, baseRect  
) where  

Shape(..) のようにすることで、Shape の全ての値コンストラクタを export します。これはつまり、module を import さえすれば誰でも、Rectangle, Circle の値コンストラクタを利用して、Shape を生成できることを意味しています。これは以下の記述と同じです。

Shape (Rectangle, Circle)

また、export のステートメントに Shape とだけ記述することで値コンストラクタを export しないこともできます。そうすることで利用者はShape の生成を補助関数の baseCircle, baseRect を使用することに限定できます。Data.Map はこのアプローチを使っています。値コンストラクタが export されていないので されていないので Map.Map [(1,2),(3,4)] のようなことはできません。Map.fromList のような補助関数を使ってマッピングできます。

値コンストラクタは、フィールドをパラメータとして受け取り、結果として何らかの型の値を返す関数に過ぎません。なので export しなければ module を import した人がこれらの関数を使用するのを防ぐことになります。データ型の値コンストラクタを export しないことで、実装を隠し、抽象化を強めることができます。また module を使用するユーザーは、値コンストラクタに対してパターンマッチングを実行できません。


まとめ

  • data キーワードを使い代数的データ型を独自に定義できる。
  • 値コンストラクタは フィールドをパラメータを引数にとりデータ型を返す関数として定義される。
  • 中間データ型を作るとモデルが洗礼され、理解の助けになる。
  • module として export することで他の箇所から利用できるようになる。
  • 値コンストラクタは export せず、代わりに補助関数を export し利用してもらうことで実装を隠蔽し、抽象化できる。

次回に続きます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1