てきとうなメモ

本の感想とか技術メモとか

Haskellの勉強 (6) - ユーザ定義型

新しい型をユーザが定義する方法

型シノニム宣言 (type synonym declarations)

型シノニムとは既存の型の別名である.例えばStringは[Char]の別名である.型シノニムはtypeを用いて宣言する.

type String = [Char]

これは単に型に別名を付けているだけであり,Cのtypedefのようなものである.

代数的型宣言 (algebraic datatype declarations)

ユーザが新しく型を定義したい時はdataを用いて宣言する.これは代数的型宣言と呼ぶ.例えば曜日の型は以下のように定義できる.

data Day = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday

これは型Dayの値はSundayからSaturdayのどれかであることを示している.この時Dayは型を生成しているので型構築子(type constructor)と呼び,Sunday...Saturdayはなんらかのデータを生成しているのでデータ構築子(data constructor)と呼ぶ.

既存の型も同様に定義でき,例えば型Boolは以下のように定義される.

data Bool = True | False

Maybe

型構築子やデータ構築子は引数をとることができる.そのことによって,既存の型を用いた型を定義できる.例えば,型Maybeは以下のように定義される.

data Maybe a = Just a | Nothing

これは,ある型aに例外としてNothing(何もない)を追加したような型である.例えば以前説明したように,lookup関数などに利用される.

再帰的な宣言

再帰を用いて再帰的な型を表現することができる.例えば,リストは以下のように表現できる.

data Eq a => List a = Nil | Cons a (List a) deriving (Eq, Show)

そうすることで,carやcdrは次のように定義できる.

car Nil = error "empty list"
car (Cons x xs) = x

cdr Nil = error "empty list"
cdr (Cons x xs) = xs

ラベル付きフィールド (labelled fields)

座標を表すPointという型を考える.

data Point a = Point a a

この例のように,データ構築子と型構築子は同じ名前でも構わない.これに対し,x座標,y座標を取得する関数pointx, pointyは以下のように定義できる.

pointx (Point x _) = x
pointy (Point _ y) = y

これはデータ構築子の場所を指定することで各座標にアクセスしている.しかし,ラベルを用いて名前でアクセスすることも出来る.こうすると,アクセサを簡単に定義することができる.フィールドは{}を用いて宣言される.

data Point a = Point { pointx, pointy :: a }

これを用いてアクセスしてみる.

> Point 1 2
Point {pointx = 1, pointy = 2}
> pointx (Point 1 2)
1
> pointy (Point 1 2)
2

正格性フラグ (strictness flags)

Haskellのデータ構造は遅延性(laziness)を持つ.データ構造の構成要素は必要になるまで評価されない.例えば

bot = bot

として

car (Cons 1 bot)

の場合botは評価されずに,1を返す.遅延データ構造の各フィールドはサンク(thunk)と呼ばれる構造に包み込まれており,必要になるまでサンクの中に入ることはない.しかし,サンクにはさまざまなオーバーヘッドが存在する.このオーバーヘッドを避けるために,遅延評価を行わないデータ構造を定義することも可能である.!でフィールドをマークすることですぐに評価を行うことができる.そのため!は正格性フラグと呼ばれる.例えば,

data Eq a => List a = Nil | Cons a !(List a) deriving (Eq, Show)

とすると

car (Cons 1 bot)

は無限ループに陥る.

どのような時に正格性フラグを付けても良いかは難しい.例えば,有理数型Ratioや複素数型Complexでは

data  (RealFloat a)     => Complex a = !a :+ !a  deriving (Eq,Read,Show)
data  (Integral a)      => Ratio a = !a :% !a  deriving (Eq)

のように定義されている.これは片方のフィールドがエラーであるならば,データ構造全体も意味を持たないのでエラーと見なしてもよいと考えられるからである.

newtype宣言

あるデータ構造を元に別のデータ構造を定義する時に用いるのがnewtype宣言である.データ構造は同じだが何か条件をつけたいときなどに用いると思われる.

A Gentle Introduction to HaskellのサンプルではIntegerを用いて自然数型Naturalを再定義している.

newtype Natural = MakeNatural Integer deriving (Eq, Show)

toNatural :: Integer -> Natural
toNatural x | x < 0 = error "Can't create negative naturals!"
            | otherwise = MakeNatural x

fromNatural :: Natural -> Integer
fromNatural (MakeNatural i) = i

instance Num Natural where 
    fromInteger = toNatural
    x + y = toNatural (fromNatural x + fromNatural y)
    x - y = let r = fromNatural x - fromNatural y in
            if r < 0 then error "Unnatural subtraction"
                     else toNatural r
    x * y = toNatural (fromNatural x * fromNatural y)
    abs x = x
    signum x = toNatural (signum (fromNatural x))

Naturalは計算結果が負の整数になるときエラーを出力する

> (MakeNatural 5) + (MakeNatural 3)
MakeNatural 8
> (MakeNatural 2) - (MakeNatural 3)
MakeNatural *** Exception: Unnatural subtraction
> (MakeNatural 2) * (MakeNatural 3)
MakeNatural 6