|
|
||
第二部も折り返し。ここから急にhaskell密度が濃くなっている感触。正直なとこ、この章の内容をちゃんとやろうとしたら一章分どころか二章あっても足りないと思う。今回のレジュメは後半息切れしています。
さて、この章では型と型クラスの話が登場。特に、ユーザーが型を定義できるdata宣言と、型クラスにまつわるclass,instance,deriving宣言が紹介される。
この話は前にもでてきたし、ここまで読んでくれば特にひっかかるところもないと思う。関数だけでなく式に型宣言がつけられるのはここで初めてでてきた?p.222の例、
luckyNumber = (7::Int) unluckyNumber = (13::Integer)
みたいに書けます。あと強いていうなら、haskellでは型の内部に変数を持つような「多相型」が使えますよ、というのがポイントか。あとは「静的に型推論します」という話が書いてある。
haskellで新しい型を定義する方法の紹介。
haskellでは代数的データ型を使って複合した型をユーザーが定義できる。「代数的データ型」というのはhaskellに限らない型理論界隈の用語で、haskellではdata宣言でそれが使える。
何が「代数的」かというと、型の和と積の組みあわせで複合的な型を構成していくところ。定義したデータ型は、型Aまたは型Bですよ、というのが型の和で、定義したデータ型は、型Aと型Bのフィールドを持ちますよ、というのが型の積。ORとANDというほうがしっくりくる?。(無理矢理)Cでいうと、型の和がenumで、型の積がstruct、あわせるとunionになる。多分、ML系言語なら同じような型定義ができたと思う。
本書では、構造体型、列挙型、そしてそれを組みあわせた共用体型がdata宣言の一般形です、というふうに順を追った説明になっているが、要はdata宣言を分解するとこれらの側面をもってますよ、ということ。
-- フィールドラベル付きの宣言でも data Anchor = Anchor {aUrl::String,aLabel::String} deriving Show -- ラベルなしでも値が生成可能。順番に注意。 *Main> Anchor "a" "b" Anchor {aUrl = "a", aLabel = "b"}
最後(p234)に、一般的なdata宣言がでてくる。今までの組みあわせ。一般的なdata宣言の構造を理解する上では、「和」の要素には、必ずデータコンストラクタがひとつだけ必要というのを意識するのがポイント。あと、型コンストラクタとデータコンストラクタは全く別物であることを意識しないと非常にわかりにくいので注意。
困ったことに、型コンストラクタとデータコンストラクタはdata宣言の両辺で似たような位置に置かれる上、同じ名前が使われる*1場合もある。理解が不十分だと、そういう例をみたときに混乱することうけあい。
ややこしい例としてはこんなの。
data Point a = Point a a
左辺のPointは「型」コンストラクタであり、例えば"Point Int"で「型」になる。右辺のPointは「データ」コンストラクタであり、例えば"Point 5 3"で「データ」になる。この二つのPointはたまたま同じ名前にしているだけで、意味するところは大きく違う。
haskellにはユーザーが型を定義する宣言はdata宣言の他にあと二つある。type宣言とnewtype宣言。
まず、data宣言というのは(見た目だけでなく)実行時に(内部的に)新たなデータ型を導入するものである、というのが大前提。要はデータ型を導入したことによるオーバヘッドがある。C++のクラス定義みたいなもの。一方、残りの二つは実行時には新たなデータ型は導入されない。表面的には新しいデータ型が導入されているようにみえるだけ。Cでいえばマクロ。
というのを踏まえて…
型に別名をつけるのがtype。いわゆるtypedef。タプルを使えば構造体型のdata宣言は模擬できそうだが、列挙型のほうは無理。当然ながらデータコンストラクタがないので、データコンストラクタを使ったパターンマッチはできない。
newtypeはdata宣言に「列挙型になってなくて、かつフィールドも一個」という制約をつけたもの。data宣言で導入したデータ型と同じように扱える。ただし、実行時には新しい型が導入されたとは見做されないので軽い…(らしい)。
newtypeの使いどころは良くわからない。
型クラスとは、「型」の「クラス」であって「型」とは別物。複数の型を表現したいが、任意の型では困る時に使う。型変数への制約として記述される。例えば、同値性の判断は任意の型で行えるわけではない。同値性判断可能な型は、Eqの型クラスに属している。
*Main> :t (==) (==) :: forall a. (Eq a) => a -> a -> Bool -- (Eq a) => の部分が型クラスによる制約
型クラスの実体は、Javaでいうところのインターフェイス。その型クラスのインスタンスである型が実装すべき関数(これをクラスメソッドと呼ぶ)の型の集合で定義される。実装は必須でないが、デフォルト実装も定義可能。
ある型クラスにある型を属させる場合には、クラスメソッドの実装が必要。デフォルト定義されていたら省略可能。型ごとに実装を書きわけられるので多重定義が実現できる。
継承もできます。
型クラスを定義するには、class宣言を使う。(p241,p242参照)
ある型をある型クラスのインスタンスであると定義する宣言。(p242参照)
デフォルト実装があるといっても全て省略できるわけでないことに注意。Eqの(==)と(/=)のように、片方をインスタンスで実装することを期待した循環したデフォルト実装もある。
データ型を定義したときに、deriving宣言と型クラスをつけることによって、ある型クラスに属するというinstance宣言を省略できる。ただし、万能ではなく、使える型クラスに制限があり、クラスメソッド実装が自明な場合のみ。"deriving Show"は便利。
もし T が次のように定義された代数的データ型であるなら
data cx => T u1 ... uk = K1 t11 ... t1k1 | ...| Kn tn1 ... tnkn deriving (C1, ..., Cm)(ここで m>=0 および括弧は m=1 の場合省略される)、 導出インスタンス宣言は、クラス C について、以下の条件がなりたてば、可能となる。
上の条件を全て満す場合は、derivingが使える。2の条件の書き方が良くわからなかったが、「derivingしたい型がEq,Ord,Show,Readであって、かつ新たに定義したデータ型の要素となる型がそれらの型クラスに属していればderivingできる。EnumとBoundedの場合はデータ型の形式に制約がある」というように理解した。個人的にはReadもいけるのが驚き。
data P = P Int deriving Show data C = C P deriving Show
みたいなderivingはOK.
1
data Line = L Int String deriving Show
2
*Main> (L 10 "hogehoge") L 10 "hogehoge"
3
data Line = L {number::Int,string::String} deriving Show
4
linelist = [L 2 "two",L 1 "one",L 3 "three",L 5 "five",L 4 "four"] *Main> linelist [L {number = 2, string = "two"},L {number = 1, string = "one"},L {number = 3, string = "three"},L {number = 5, string = "five"},L {number = 4, string = "four"}]
5
Lineを型クラスOrdのインスタンスにしてみた。この章の流れで模範解答(http://i.loveruby.net/ja/stdhaskell/samples/line.hs.html)がそうなってないのは不思議。
import List data Line = L {number::Int,string::String} deriving (Show,Eq) instance Ord Line where compare L {number=a} L {number=b} = compare a b {- -- この仕様ならderiving Ord一発でもOK -- ただし、data宣言のnumberとstringの位置が逆だとderiving Ordでは駄目 data Line = L {number::Int,string::String} deriving (Show,Eq,Ord) -} sortLines :: [Line] -> [Line] sortLines = List.sortBy compare -- sortLines = List.sort でも可。なので実質的にはderiving Ord書けばそれだけでよい。 linelist = [L 2 "two",L 1 "one",L 3 "three",L 5 "five",L 4 "four"] *Main> sortLines linelist [L {number = 1, string = "one"},L {number = 2, string = "two"},L {number = 3, string = "three"},L {number = 4, string = "four"},L {number = 5, string = "five"}]