Haskell: type と typeclass を作る
よく「えっとどうだっけ」ってなるので,何回かに分けて書いて整理しようと思います.
以下は,Learn You a Haskell for Great Good! の Chapter 8, Making Our Own Types and Typeclasses を元にしたものです.基本的には翻訳のつもりですが,省略や加筆が勝手に入ることもあります.原文は CC-BY-NC-SA でライセンスされており,この文書もそれに従います.
Algebraics data types
ここまで,いろんな data type を見てきた.例えば Bool
, Int
, Char
, Maybe
とかがあったよね.じゃあ自分で作るときはどうしたらいいんだろう.
ひとつのやり方は keyword になっている data
を使うことだ.例えば Bool
が標準ライブラリでどのように定義されているかを見てみよう.
data Bool = False | True
data
ってのは新しい data type を作ってるよってこと. =
の前の部分が type を表してて,この場合は Bool
だ.
=
の後の部分が value constructors. ここではこの type が持ちうる値を規定している. |
は「または」と読んでいい.
だから全体ではこうなる:Bool
type は False
か True
の値を持てる1.type の名前と value constructor は両方大文字で始まっている必要がある.
同じように, Int
という type はこういうふうに定義されてると考えることができる.
data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647
-2147483648
, 2147483647
が上限と下限,足したり引いたりすると循環する(succ
は Exception).
実際にはこうなってるわけではないけど,まあ,という感じだ.
さて,Haskell で単純な図形をどう表現するか考えてみよう.tuple を遣うのはひとつの手で,例えば円なら (43.1, 55.0, 10.4)
という感じで (x,y,r)
で表すことが出来る.悪くなさそうだけど,単なる数の三つ組なら3次元ベクトルかもしれないし,他のあらゆるものと区別がつかない.
そこで自分で type をつくろうという話になるわけだ.円か長方形なら,こういう感じ
data Shape = Circle Float Float Float | Rect Float Float Float
ここまで書けば,ghci で
Prelude> data Shape = Circle Float Float Float | Rect Float Float Float Float
Prelude> :t Circle
Circle :: Float -> Float -> Float -> Shape
Prelude> :t Rect
Rect:: Float -> Float -> Float -> Float -> Shape
なるほどなるほど,value constructor はこれもまた function なわけね.なかなか.
では shape
を受け取ってその面積を求める函数を書いてみよう:
surface :: Shape -> Float
surface Circle (_ _ r) = pi * r^2
surface Rect (x0 y0 x1 y1) = abs $ (x0-x1) * (y0-y1)
pattern match 出来るんだ!すごい!……凄いんだけど,実はこれはしょっちゅうやってきたことで,False
とか 5
とか []
とか,
全部たまたま field を持たなかっただけで,結局は一緒なんだ.
気をつけるべきは Circle -> Float
とかは書けないこと. Circle
は type じゃない.丁度 True -> Float
がかけないようなものだ.
この type と constructor の違いは多分こまめに意識しといたほうがいい.
ところで,このままでは ghci
で Circle 3 3 4
とかやった時に不便だから,こうするといい感じ.
data Shape = Circle Float Float Float | Rect Float Float Float Float deriving (Show)
これで show
を使えるし, ghci
でも
Prelude> Circle 4 4 45
Circle 4.0 4.0 45.0
ふむふむ.
なかなかいい感じだけど,ちょっと改善できそうなところもある.例えば今円は (x,y,r)
で表してるけど,( (x,y), r)
みたいな方が把握しやすい.
data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rect Point Point deriving (Show)
ここで data Point = Point Float Float
と, value constructor と data type の名前に同じものを使ったが,
これは別にそうである必要はない.ただ constructor が1種類しかない場合はそうするのが慣例ということだ.
こうすると surface
は
surface :: Shape -> Float
surface (Circle _ r) = pi*r*r
surface (Rect (Point x0 y0) (Point x1 y1)) = abs $ (x0-x1) * (y0-y1)
同じ感じで図形を動かす nudge
と原点にものを作る baseCircle :: Float->Shape
, 同様な baseRect
が定義できる.
これらをモジュール化するならこう:
module Shapes
( Point(..)
, Shape(..)
, surface
, nudge
, baseCircle
, baseRect
) where
ここで たとえば Shape(...)
を単に Shape
と書けば import したひとは constructor を使えなくなり,
補助函数 baseCircle
と nudge
であれこれするようになる.プライベートなアレっちゅうこっちゃな.
…ということで長くなったので今日はここまで.
–
-
この順になっているから
succ False
はTrue
を返すけど,succ True
は Exception. ↩