読者です 読者をやめる 読者になる 読者になる

成らぬは人の為さぬなりけり

エンジニアライフをエンジョイする為のブログ

すごいHaskellたのしく学んでみる-その6

先週サボったので、二週間ぶりです。

今日のテーマは、、、

  • データ型
    • レコード構文
    • 型引数
  • 型クラス

今までずっとオブジェクト指向でやってきた自分にとっては、
ここはなかなか理解しづらく、
未だに0から自分で書ける気がしない、、、

データ型

データ型というのは、値の事。
つまり、IntやBool,Char,Maybeなどなど。

自分でデータ型を定義してみます。

type Name = String
data Person = Man Name | Woman Name

name :: Person -> String
name (Man name) = "Mr." ++ name
name (Woman name) = "Ms." ++ name

一行目は、型シノニムを使って、文字列にNameという別名をつけました。
二行目で男性と女性を表す値コンストラクタを定義しています。
では、実行してみたいと思います。

*Main> name $ Man "hoge"
"Mr.hoge"
*Main> name $ Woman "hoge"
"Ms.hoge"

うん、意図通りに動作しました。
Haskellオブジェクト指向ではないので、
nameという値を持つデータに対して、

hoge.name

のようにアクセスしません。

パターンマッチを使って、コンストラクタに渡した値を取り出します。


この辺がオブジェクト指向に慣れている自分には
非常に違和感がある所でもありました。

上の例では、StringにNameという別名をつけましたが、
これは、「Man String」と書かれても、その文字列は何?
というのがわかりづらいので、別名をつけました。
しかし、今回は引数が一個だから良いものの、
もっと増えてきたら、全部別名つけるの???
Haskellにはレコード構文というのが用意されています。

レコード構文

レコード構文を使えば引数に名前をつけることができます。

data Employee = Employee { name :: String
                         , age :: Int
                         , address :: String
                         , unit :: String
                         } deriving (Show, Eq)

なんとなく、ここの宣言では、関数の型を定義していて、(→name::String)
実際にコンストラクタを呼ぶ時に関数の実装を渡すみたいなイメージなのかな、(name="hoge")
と勝手に思ってみたりしています。

使ってみます。

*Main> Employee{ name="hoge", unit="foobarbaz", age=90, address="tokyo"}
Employee {name = "hoge", age = 90, address = "tokyo", unit = "foobarbaz"}

いわゆる名前付き引数なので、順番は変えても大丈夫のようです。

レコード構文を使うと、自動的にフィールドを取得する関数が追加されます。

*Main> let hoge = Employee{ name="hoge", unit="foobarbaz", age=90, address="tokyo"}
*Main> name hoge
"hoge"
*Main> unit hoge
"foobarbaz"
*Main> age hoge
90
*Main> address hoge
"tokyo"

型引数

javaで言うジェネリクスのようなイメージでしょうか、
型を引数で渡します。

HaskellにはMaybeという型があります。
Scalaで言うOptionのような感じ)

data Maybe a = Nothing | Just a

Maybeは引数にaを取ります。このaが型引数です。
Maybeは型引数を取っているので、「型コンストラクタ」と呼ばれるそうです。

(値コンストラクタと型コンストラクタはいつもわからなくなる、、、orz)

data Box a = EmptyBox | Box a deriving (Show)

box_add :: (Num a) => Box a -> Box a -> Box a
box_add EmptyBox addend = addend
box_add augend EmptyBox = augend
box_add (Box x) (Box y) = Box $ x + y

Boxが型コンストラクタというのは、box_addの型宣言を見ると、
なんとなく納得できます。
型引数が無しの型の場合

hoge :: Int -> Int

このIntの所は型が来るわけですが、
型引数がある場合は、Intの所にBox aのように書きます。
つまり、「Box a」という「型」なわけです。
なので、aを引数に取って、Box aという型を作る、型コンストラクタ、になるわけですね。
うむ。


さて、インスタンス自動導出はあまりおもしろくなかったので、
型クラスを自分で作ってみようぜ、
というお話に入りたいと思います。

型クラス

型クラスは、Javaで言えばインターフェースのような物、らしいです。
型クラスの振る舞いは型クラス関数として定義します。


(手続き型言語のクラスのことは全部忘れろって書いてあるけど、
 忘れちゃったら仕事にならないので、それは無理っすw)


Eq型クラスを例に書いてみたいと思います。
まず、Eq型クラスの宣言。

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)

はい、よくわからないです。
自分は今でもよくわからないですが、
Eqは関数の型宣言だけでなく、デフォルト実装も入っているようです。
ScalaのTraitに近いのかな)
2〜3行目は型の宣言で、
4〜5行目は、「==は/=の逆で、/=は==の逆です」と書いてあるそうです。
(相互再帰というらしい)
これによって、この型クラスのインスタンスは、
「==」「/=」のどちらかを実装すればよくなるそうで、
こういう定義を、「最小完全定義」というらしいです。
言っていることは理解できるんですが、
なんか本質的な所が理解できていないというか、、、
ん〜なんかひっかかる。
とりあえず先に進みたいと思います。


では、こいつのインスタンスを定義してみたいと思います。
さっきのBoxをEq型クラスのインスタンスにしてみたいと思います。

data Box a = EmptyBox | Box a deriving (Show)

instance (Eq a) => Eq (Box a) where
    EmptyBox == EmptyBox = True
    Box x == Box y = x == y
    _ == _ = False

ここにはいくつかの要素が含まれています。
1. Eqの引数は型
 まず、Eqの引数は型なので、Boxは渡せません。
 Boxは型コンストラクタなので、「Box a」で型になります。
2. Boxのフィールドの比較
 boxが等価かどうか比較する為に、フィールドの値を比較する必要があります。
 ということは、aもEqのインスタンスである必要があるため、
 aにEqの型制約をつけます。

では、比較してみましょう。

*Main> EmptyBox == EmptyBox 
True
*Main> EmptyBox /= EmptyBox 
False
*Main> Box 10 == Box 1
False
*Main> Box 10 == Box 10
True


今日はここまでにしたいと思います。

次回は、何をやろうかな、、、
本自体は、読み進んでいるんですが、
なかなか理解できないので、何度も読み返していると、
なかなか進まない、、、orz
がんばります。