型
Haskellでは 型 とはプログラム内で用いるデータを表現するものです。
データの宣言[edit]
Haskellでは型をdata
宣言を通じて導入、または宣言します。一般的にデータ宣言はこのように行います。
data [context =>] type tv1 ... tvi = con1 c1t1 c1c2... c1tn | ... | conm cmt1 ... cmtq [deriving]
まだHaskellをあまり理解していいない方にはおそらく説明になっていないかもしれません。
上の宣言の本質は、data
キーワードを使って、付属的なコンテキストを与え、型の名前と多くのtype variableを与えることにあります。
その後、多くのconstructorが続きます。これらはtype variableかtype constantのリストになっています。最後に付属的にderiving
が続きます。
他にも多くの微妙な事柄が必要です。たとえばデータコンストラクタに必要なパラメータはeagerでなければならない、どんなclassがderivingの中で許可されているか、コンストラクタ内のfield名の使い方、contextが実際になにをするのかなどです。これらについてはそれぞれの記事を参照してください。
いくつかの例を見てみましょう。Haskellの標準データ型Maybeは普通このように宣言されています。
data Maybe a = Just a | Nothing
これは、Maybe型はaで表される1つの型変数を持っていて、JustとNothingという2つのconstructorを持っているということを意味しています。(Haskellでは型名とコンストラクタ名は大文字で始まらなければいけないことに注意してください) Justコンストラクタは1つのパラメータ"a"をとります。
他の例として、二分木 (binary Tree)を考えてみましょう。このように表されます。
data Tree a = Branch (Tree a) (Tree a) | Leaf a
ここで、Treeのコンストラクタの1つBranchはコンストラクタのパラメータには2つのtreeを取る一方で、Leafは型変数"a"だけを取ります。Haskellではこういった再帰型は非常によく使われるpatternsです。
型と新しい型[edit]
他のHaskellプログラムに型を導入する方法はtype
とnewtype
を用いて行う方法です。
type
はあるデータコンストラクタに対してシノニムを与えます。newtype
ではある型をリネームして新しいコンストラクタを作成する必要があります。
type
宣言をするときは、型シノニムとその基底型はほぼいつでも交換可能です。(instance宣言を扱う場合にいくつか制限があります)たとえば、このような宣言があったとします。
type Name = String
String
をシグネチャにもつどのような関数もName
型の要素に対して用いることができます。
しかしながら、もしこのような宣言の場合:
newtype FirstName = FirstName String
先ほどの例のようにはいきません。関数の宣言においては実際にFirstNameに対して定義されてなければいけません。このような要求を軽減するために、しばしばデコンストラクタを宣言します。こんな具合です:
unFirstName :: FirstName -> String
unFirstName (FirstName s) = s
これはよくnewtype
内のfieldを使うときに行われます。(フィールドを幅広く使う人もいる一方で、多くの人がHaskellのフィールド実装は甘いと考えています。Programming guidelinesやFuture of Haskellも参照してください)
簡単な例[edit]
たとえばトランプのゲームのブリッジを実装することを考えてみましょう。まずカードを表す何かが必要です。一つの例としてはこんなかんじです。
まず、スートと数を表すデータ型を作成します。
data Suit = Club | Diamond | Heart | Spade
deriving (Read, Show, Enum, Eq, Ord)
data CardValue = Two | Three | Four
| Five | Six | Seven | Eight | Nine | Ten
| Jack | Queen | King | Ace
deriving (Read, Show, Enum, Eq, Ord)
この例ではスートと数字を / からStringやIntに変換できるようにderiving節を使っています。これによって同値性や順序を確認できます。型変数がないような型を使うことで、同値性はどのコンストラクタが使われたかに依存し、順序は記述の順番に依存します。例えばThree
はQueen
よりも小さいです。
では実際にCard
を定義してみましょう。
data Card = Card {value::CardValue,
suit::Suit}
deriving (Read, Show, Eq)
この定義ではfieldを使っています。これによってCard
の2箇所に対してアクセスする既成の関数が手に入ります。また、もう一度いいますが、型変数(type variables)が使われていませんが、データのconstructorは2つの引数に対して特定の型、つまりCardValue
とSuit
であることを要求しています。
ここでのderiving節は単に求められている3つのClassを明記しているだけです。instance宣言をOrdやEnumに対して行います。
instance Ord Card where
compare c1 c2 | (value c1 == (value c2)) = compare (suit c1) (suit c2)
| otherwise = compare (value c1) (value c2)
instance Enum Card where
toEnum n = Card (toEnum (n `div` 4)) (toEnum (n `mod` 4))
fromEnum c = 4*(fromEnum (value c)) + (fromEnum (suit c))
最後に、Deck
型をCard
のリストに対するエイリアスとして宣言し、デッキをリスト内包(list comprehension)として表します。
type Deck = [Card]
deck::Deck
deck = [Card val su | val <- [Two .. Ace], su <- [Club .. Spade]]
追加してください[edit]
より図解が多い例を歓迎します。
参考[edit]
データconstructorやclassに関する(必要な)記事を読んでみてください。またHaskell 98 reportやご自分が選んだ実装(GHC/Documentationなど)にも最新の情報があるかもしれません。R
- Determining the type of an expression - Let the Haskell compiler compute the type of expression
- Language extensions - many language extensions are to do with changes to the type system.
- Smart constructors shows some interesting examples including a non-trivial usage of
newtype
. - Unboxed type shows ways to have values closer to the bare metal :).
- Phantom type discusses types without constructors.
- Type witness gives an example of GADTs, a GHC extension.
- Existential type shows how to implement a common O-O programming paradigm.
- Type arithmetic implements the Peano numbers.
- Reified type, Non-trivial type synonyms, Abstract data type, Concrete data type, Algebraic data type.
- Research_papers/Type_systems allow the curious to delve deeper.
Languages: en