「ふつける」勉強会 資料用 このページをアンテナに追加 RSSフィード

 | 

2006-08-05

ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) - 「ふつける」勉強会 資料用 を含むブックマーク はてなブックマーク - ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) - 「ふつける」勉強会 資料用 ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) - 「ふつける」勉強会 資料用 のブックマークコメント

第二部も折り返し。ここから急に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"}
  • 列挙型
    • "|"で区切って、"あれかこれか"を表現。Bool型も実体はTrueとFalseの二つのデータコンストラクタからなる(列挙型の)型定義である。
  • 共用体型
    • 上記二つの組みあわせ。列挙型で構造体型を並べた形式。
    • 再帰的な定義(左辺で定義している型を右辺で使う)のもあり。
型コンスタラクタとデータコンストラクタ

最後(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宣言は模擬できそうだが、列挙型のほうは無理。当然ながらデータコンストラクタがないので、データコンストラクタを使ったパターンマッチはできない。

newtypedata宣言に「列挙型になってなくて、かつフィールドも一個」という制約をつけたもの。data宣言で導入したデータ型と同じように扱える。ただし、実行時には新しい型が導入されたとは見做されないので軽い…(らしい)。

newtypeの使いどころは良くわからない。

型クラス

型クラスとは、「型」の「クラス」であって「型」とは別物。複数の型を表現したいが、任意の型では困る時に使う。型変数への制約として記述される。例えば、同値性の判断は任意の型で行えるわけではない。同値性判断可能な型は、Eq型クラスに属している。

*Main> :t (==)
(==) :: forall a. (Eq a) => a -> a -> Bool
-- (Eq a) => の部分が型クラスによる制約
型クラスの実体

型クラスの実体は、Javaでいうところのインターフェイス。その型クラスインスタンスである型が実装すべき関数(これをクラスメソッドと呼ぶ)の型の集合で定義される。実装は必須でないが、デフォルト実装も定義可能。

多重定義

ある型クラスにある型を属させる場合には、クラスメソッドの実装が必要。デフォルト定義されていたら省略可能。型ごとに実装を書きわけられるので多重定義が実現できる。

継承

継承もできます。

class宣言

型クラスを定義するには、class宣言を使う。(p241,p242参照)

instance宣言

ある型をある型クラスインスタンスであると定義する宣言。(p242参照)

デフォルト実装があるといっても全て省略できるわけでないことに注意。Eqの(==)と(/=)のように、片方をインスタンスで実装することを期待した循環したデフォルト実装もある。

deriving宣言

データ型を定義したときに、deriving宣言と型クラスをつけることによって、ある型クラスに属するというinstance宣言を省略できる。ただし、万能ではなく、使える型クラスに制限があり、クラスメソッド実装が自明な場合のみ。"deriving Show"は便利。

derivingの詳細

もし T が次のように定義された代数的データ型であるなら

data cx => T u1 ... uk 	= 	K1 t11 ... t1k1 | ...| Kn tn1 ... tnkn
		deriving (C1, ..., Cm)

(ここで m>=0 および括弧は m=1 の場合省略される)、 導出インスタンス宣言は、クラス C について、以下の条件がなりたてば、可能となる。

  1. C が EqOrd、Enum、 Bounded、Show あるいは Read のうちの どれか。
  2. cx' =>C tij が構成要素の型 tij のそれぞれについて保存されているような 文脈 cx' がある。
  3. C が Bounded である場合。このとき、この型は列挙型 (すべての構築子が無引数)であるか、または構築子が一つしかないかの どちらかでなければならない。
  4. C が Enum である場合。このとき、この型は列挙型 でなければならない。
  5. T u1 ... ukを C のインスタンスとしたプログラム内の別の個所で明示的な インスタンス宣言があってはならない。

The Haskell 98 Report: Derived Instances

上の条件を全て満す場合は、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.

主要な型クラス
    • Eq,Ord
    • Show
    • Read
    • Num,Integral,Fractional

練習問題

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"}]

*1名前空間が分離されているので同じ名前でも合法

 |