ANA国内線【PR】
RWH の読み方(47) 第6章
Real World Haskell 第6章 Using Typeclasses

Living in an Open World

このセクションではタイプクラスの設計が open world になるように設計されているということを説明している。つまりタイプクラスにインスタンスをどこからでも追加できるということだ。

モジュールのようなコンテクストの中でオーバーロードできるインスタンスが決まっているのを closed world と言うらしい。open world と closed world は演算子/関数のオーバロードの方式に関する専門用語らしいが、読みこなせなかった。

このセクションの最初のコード例は、toValue や fromValue のオーバーローディングがいつでもできますよという例だ。

後半の2つのコード例は、次のセクションに関連してくる。

タイプクラスによる関数のオーバーローディングのどの定義を使うかは引き数の型によって選択されるが、曖昧な引き数が複数の型に解釈できるときに、Overlapping instances エラーが発生する。そういうインスタンスを発生させやすい型によるオーバーロードの定義の例を示している。

When Do Overlapping Instances

このセクションでは前のセクションで定義した、[a] というインスタンス

instance (JSON a) => JSON [a] where

と [(String, a)] というインスタンス

instance (JSON a) => JSON [(String, a)] where

の2つが、[("foo", "bar")] というデータに関して overlapping instance になるため、実行時エラーになってしまうことを示している。つまり、このデータが [a] なのか [(String, a)] なのか区別できないからだ。

理屈では分かっても実際にコードをテストしてみないとわからないので test.hs というファイルにコードを記述してテストしてみる。まず、データ型 JValue を思い切り手抜きして次のように定義する。

data JValue = NotDefined deriving (Show)

これを ghci で :l test.hs でテストするとコンパイルできる。そこで、JSON クラスの宣言を追加する。

class JSON a where
  toJValue :: a -> JValue
  fromJValue :: JValue -> Either String a

これも ghci の :r コマンドでテストするとコンパイルできる。さらに、String 型を JSON クラスのインスタンスにする。

instance JSON String where
  toJValue s = NotDefined
  fromJValue = undefined

今度は、インスタンスの宣言の仕方がよくないと指摘されてしまう。

Prelude> :r
[1 of 1] Compiling Main ( test.hs, interpreted )

test.hs:7:0:
    Illegal instance declaration for `JSON String'
        (All instance types must be of the form (T t1 ... tn)
         where T is not a synonym.
         Use -XTypeSynonymInstances if you want to disable this.)
    In the instance declaration for `JSON String'
Failed, modules loaded: none.

そこで指摘のとおりにソースの冒頭に TypeSynonymInstances スイッチを ON にする pragma を書き込む。

{-# LANGUAGE TypeSynonymInstances #-}

もう一度 :r コマンドでテストしたら今度は成功した。

Prelude> :r
[1 of 1] Compiling Main ( test.hs, interpreted )
Ok, modules loaded: Main.
*Main> toJValue "hello"
NotDefined

そこで、いよいよ [a] と [(String, a)] を JSON クラスのインスタンスにする。

instance (JSON a) => JSON [a] where
  toJValue [a] = NotDefined
  fromJValue = undefined

instance (JSON a) => JSON [(String, a)] where
  toJValue [(s, a)] = NotDefined
  fromJValue = undefined

今度は :r コマンドでテストすると、次のようにインスタンスの宣言が悪いので FlexibleInstances スイッチを使うようにとのメッセージが出る。

Prelude> :r
[1 of 1] Compiling Main ( test.hs, interpreted )

test.hs:17:0:
    Illegal instance declaration for `JSON [(String, a)]'
        (All instance types must be of the form (T a1 ... an)
         where a1 ... an are type *variables*,
         and each type variable appears at most once in the instance head.
         Use -XFlexibleInstances if you want to disable this.)
    In the instance declaration for `JSON [(String, a)]'
Failed, modules loaded: none.

そこで、FlexibleInstances スイッチを pragma に追加する。

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}

再度 :r でテストするとコンパイルできるが、
*Main> :r
Ok, modules loaded: Main.

次のように toValue を [("foo", "bar")] に関数適用すると、RWH の本文に見られたような Overlapping instances エラーがでる。

*Main> toJValue [("foo", "bar")]

<interactive>:1:0:
    Overlapping instances for JSON [([Char], [Char])]
      arising from a use of `toJValue' at <interactive>:1:0-24
    Matching instances:
      instance (JSON a) => JSON [a] -- Defined at test.hs:13:9-28
      instance (JSON a) => JSON [(String, a)]
        -- Defined at test.hs:17:9-38
    In the expression: toJValue [("foo", "bar")]
    In the definition of `it': it = toJValue [("foo", "bar")]

ここまでで、このセクションの内容は再現できたが、次のセクションに出てくる OverlappingInstances 言語拡張を pragma に記述してみる。

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, OverlappingInstances #-}

するとインスタンスの overlapping の問題も解決されて、めでたく toValue [("foo", "bar")] が実行される。

*Main> :r
[1 of 1] Compiling Main ( test.hs, interpreted )
Ok, modules loaded: Main.
*Main> toJValue [("foo", "bar")]
NotDefined

タイプクラスを実際のプログラムで活用するためには、Haskell 98 report の仕様だけでは使い難いようだ。この場合でも、GHC の言語拡張をソースの冒頭の部分で pragma に記述することで使い易くなるのが分かる。

蛇足だが、テストプログラムの全文を次に示す。

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, OverlappingInstances #-}

data JValue = NotDefined deriving (Show)

class JSON a where
  toJValue :: a -> JValue
  fromJValue :: JValue -> Either String a

instance JSON String where
  toJValue s = NotDefined
  fromJValue = undefined

instance (JSON a) => JSON [a] where
  toJValue [a] = NotDefined
  fromJValue = undefined

instance (JSON a) => JSON [(String, a)] where
  toJValue [(s, a)] = NotDefined
  fromJValue = undefined
# by tnomura9 | 2012-05-22 06:45 | Haskell | Trackback | Comments(0)
きょお☆
ストロボナイツ

【きょお☆】 ストロボナイツ 【踊ってみた】

ソロ

【きょお☆】教えて!!魔法のLyric【おまけあり♡】
【お誕生日記念♪】ハッピーシンセサイザ踊ってみた【きょお☆】
【きょお☆】Gravity=Reality踊ってみた【小雨の中】

コラボ

【きょお☆】くれよん / Crayon 踊ってみた【ちえ】HD
# by tnomura9 | 2012-05-21 05:02 | ボーカロイド | Trackback | Comments(0)
RWH の読み方(46) 第6章
Real World Haskell 第6章 Using Typeclasses

第6章ではあまり実際のコードのテストをしていなかったのでおさらいをしてみる。

まず、自前のタイプクラスの使い方の演習。

ファイル名:eqclasses.hs

class BasicEq a where
  isEqual, isNotEqual :: a -> a -> Bool

  isEqual x y = not (isNotEqual x y)
  isNotEqual x y = not (isEqual x y)

instance BasicEq Bool where
  isEqual True True = True
  isEqual False False = True
  isEqual _ _ = False

BasicEq タイプクラスを定義して、generic funcition として isEqual と isNotEqual を定義している。次に instance キーワードで Bool 型を BasicEq タイプクラスのインスタンスに登録する。

実行例:

*Main> :l eqclasses.hs
[1 of 1] Compiling Main ( eqclasses.hs, interpreted )
Ok, modules loaded: Main.
*Main> isEqual True True
True
*Main> isNotEqual True True
False

次は、Typeclasses at Worl: Making JSON Easier to Use セクションの実習。

まず、SimpleJSON モジュールが必要なので再掲する。

ファイル名:SimpleJSON

-- file: chap05/SimpleJSON
module SimpleJSON
  (
    JValue(..)
  , getString
  , getInt
  , getDouble
  , getObject
  , getArray
  , isNull
  ) where

data JValue =
    JString String
  | JNumber Double
  | JBool Bool
  | JNull
  | JObject [(String, JValue)]
  | JArray [JValue]
    deriving (Eq, Ord, Show)

-- accessor functions
getString :: JValue -> Maybe String
getString (JString s) = Just s
getString _ = Nothing

getInt :: JValue -> Maybe Int
getInt (JNumber n) = Just (truncate n)
getInt _ = Nothing

getDouble :: JValue -> Maybe Double
getDouble (JNumber n) = Just n
getDouble _ = Nothing

getBool :: JValue -> Maybe Bool
getBool (JBool b) = Just b
getBool _ = Nothing

getObject :: JValue -> Maybe [(String, JValue)]
getObject (JObject o) = Just o
getObject _ = Nothing

getArray :: JValue -> Maybe [JValue]
getArray (JArray a) = Just a
getArray _ = Nothing

isNull v = v == JNull

JSON タイプクラスの宣言と、JBool 型についての toJValue 関数と、fromJValue 関数のテスト。

ファイル名: JSONClass.hs

import SimpleJSON

type JSONError = String

class JSON a where
  toJValue :: a -> JValue
  fromJValue :: JValue -> Either JSONError a

instance JSON Bool where
  toJValue = JBool

  fromJValue (JBool b) = Right b
  fromJValue _ = Left "not a JSON boolean"

実行例は次のようになる。toJValue はうまく動いた。

*Main> :l JSONClass.hs
[1 of 2] Compiling SimpleJSON ( SimpleJSON.hs, interpreted )
[2 of 2] Compiling Main ( JSONClass.hs, interpreted )
Ok, modules loaded: SimpleJSON, Main.
*Main> toJValue True
JBool True

ところが、formValue を試すと formValue のタイプパラメータの型が曖昧だというエラーになってしまう。

*Main> fromJValue (JBool True)

<interactive>:1:0:
    Ambiguous type variable `a' in the constraint:
      `JSON a' arising from a use of `fromJValue' at :1:0-22
    Probable fix: add a type signature that fixes these type variable(s)

困ってしまって、"Real World Haskell fromJValue not work" で Google 検索していたら、戻値の型指定をすれば良いと教えてもらった。read の時と状況が似ている。

*Main> fromJValue (JBool True) :: Either JSONError Bool
Right True
*Main> fromJValue (JNumber 3) :: Either JSONError Bool
Left "not a JSON boolean"

動作確認ができたので、次のセクションに読み進めることができるようになった。
# by tnomura9 | 2012-05-20 12:36 | Haskell | Trackback | Comments(0)
@小豆
ソロ

【@小豆】Yurufuwa Jukai Girl ゆるふわ樹海ガール【踊ってみた】HD
【@小豆】Sadistic Love サディスティック・ラブ踊ってみた【JKラスト】HD
【@小豆】Heart Beats【踊ってみた】HD
@小豆 教えて!!魔法のLyric【踊ってみた】
【再うp】踊ってみた@小豆【せーのっ】
【@小豆】 『プラチナ』-shin'in future Mix- 【踊ってみた】

# by tnomura9 | 2012-05-20 08:25 | ボーカロイド | Trackback | Comments(0)
The World Warrior
ここから、

【ゆりにゃ】The World Warrior (rap ver.)【踊ってみた】

ここへ来て、

The World Warrior street fighter 2 hyadain HD

ここへたどり着いた。

Hyadain/ヒャダイン


元気が出ました。ヒャダインさんありがとう。
# by tnomura9 | 2012-05-19 17:36 | ボーカロイド | Trackback | Comments(0)
RWH の読み方(45) 第6章
Real World Haskell 第6章 Using Typeclasses

Typeclasses at Work: Making JSON Easier to Use

このセクションでは、まず、簡単なJSONのデータをHaskellの形式に翻訳している。次に元々のJSONの記述と Haskell のデータ型に翻訳したものを引用する。

JSON版

{
  "query": "awkward squad haskell",
  "estimatedCount": 3920,
  "moreResults": true,
  "results":
  [{
    "title": "Simon Peyton Jones: papers",
    "snippet": "Tackling the awkward squad: monadic input/output ...",
    "url": "http://research.microsoft.com/~simonpj/papers/marktoberdorf/",
   },
   {
    "title": "Haskell for C Programmers | Lambda the Ultimate",
    "snippet": "... the best job of all the tutorials I've read ...",
    "url": "http://lambda-the-ultimate.org/node/724",
   }]
}

Haskell 版

result :: JValue
result = JObject [
  ("query", JString "awkward squad haskell"),
  ("estimatedCount", JNumber 3920),
  ("moreResults", JBool True),
  ("results", JArray [
     JObject [
      ("title", JString "Simon Peyton Jones: papers"),
      ("snippet", JString "Tackling the awkward ..."),
      ("url", JString "http://.../marktoberdorf/")
     ]])
  ]

Haskell 版の場合、リストの要素の型は全て同じでなくてはならないなどのHaskellの型システムの制約から、データは全て JValue のデータコンストラクタによってラッピングして使用しないといけない。

そのため JSON の要素の置き換えや作成などを行うときも JNumber や JString などの複数のデータコンストラクタを使い分けなければならない。これではデータ操作の関数を記述するときに関数の記述が複雑になってしまう。一方型付けのないシステムでは、そういう詳細の検討はいらないので、どんなタイプのデータも同じ関数で処理できて、表記が簡単になる。(しかしランタイムのバグが紛れ込む可能性が高い。)

そこで狙いとしては、toValue x という関数を定義して、x の型を toValue が識別してデータコンストラクタを自動的に選択するようにしたい。Show クラスの関数の show が引き数の型を自動判別して文字列に変換するのに似ている。

また、fromValue :: JValue というように関数の戻値を :: (type signature) で指定することでコンテナの値を formValue 関数だけで取り出したい。これは、Read クラスの関数 read が文字列を type signature にしたがって様々な型のデータに変換するのと似ている。

ただし、read クラスの場合は引き数が全て文字列だったが、fromValue の場合は引き数は JValue 型なので type signature の指定とコンテナのデータ型が合っていない場合はエラーになる。そこで、formValue では戻値を Either 型にすることで、プログラムを止めずにエラー処理できるように工夫してある。

このように、タイプクラスのシステムを利用することで、煩雑になりがちなデータ型ごとの処理を関数でひとつにまとめることができる。

More Helpful Errors

このサブセクションでは、Either 型について述べてある。Either 型は標準で組み込まれており、関数の実行が失敗するような場合の処理に利用されている。Maybeと似ているが、Maybeは関数の実行が失敗した時単に Nothing を返すだけだが、Either 型では、Left と一緒に文字列のメッセージを返すことができる。

次に Either 型のデータ型の定義をを示す。

data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

Either a b とあるが別にデータコンストラクタがパラメータを2つ取るという意味ではなく、a 型と b 型という2つの型が利用されるという意味だ。

これを使って fromValue の実行が失敗した時にエラーメッセージを返すようにするためには次のようにする。

instance JSON Bool whrere
  toJValue = JBool
  fromJValue (JBool b) = Right b
  fromJValue _ = Left "not a JSON boolean"

Making an Instance with a Type Synonym

このサブセクションではインスタンス宣言をする時に次のように type synonym を使えず、インスタンスがリストのとき、リストの要素としては type parameter のみが許されることを説明している。例えば次のような instance 宣言は許されない。

instance JSON String where

String は [Char] の type synonym (型の別名)だからだ。タイプクラスのインスタンスとして [a] は使えるが、[Char] は使えない。Haskell 98 Report の文法の性質から、type synonym はインスタンスとして使えないし、また、 リストも [a] のように type parameter を使ったリストではない [Char] 等のリストはインスタンスとして使用できないからだ。

Haskell 98 Report の文法を読んでみたが、部分的な抜粋だったのかよくわからなかった。レポート全体を理解して読むのは無理のような気がする。

Haskell 98 Report では文法を規定しているだけだが、それに準じて実装すると、文法の範囲以外のものはエラーになってしまう。しかし、GHC は拡張機能を持っており、ソースにコンパイル時の行動を指示する次のような pragma を記述しておくと、type synonym もインスタンスとして使うことができる。

{-# LANGUAGE TypeSynonymInstances #-}

単純なデータ型を使うタイプクラスの使い方は割にシンプルだが、複雑なデータ型になってくると上のように運用が難しくなる。後のサブセクションでは、型同士の衝突について述べてあるようだ。これらの知識も実際にプログラムを作ってテストしてみないと分からないが、今の段階ではとりあえず説明を読んで理解するだけにする。

上の説明にしても、Haskell 98 Report に記述されている Haskell の文法を読みこなしたり、GHC の pragma とはなんだろうと調べてみたり、脱線をかなり深くしないと本当の理解には繋がらない。それだけ、プログラム言語の理解をするということは奥深いということだが、灘高の国語の先生が授業で使った「銀の匙」という小説を題材に徹底的に脱線して題材を掘り下げるという方法が、こんな所にも直接的な形で生きている。

単に、目の前の仕事を片付けるという意味では、こういう探索の仕方は時間の浪費だが、しかし、これがなされていないと突然起こったエラーメッセージの処理などが解決できず立ち往生することにもつながる。脱線の労苦を覚悟するという態度も忘れるわけにはいかない。プログラマの生産性には100倍以上の差があるという話だが、こういう無駄な努力の積み重ねが見かけの驚異的な生産性の元になるのだろう。

著者たちのそういう知識の探索の様子が、今回のRWHの本文の理解のしがたさの端々に現れているような気がする。著者たちの簡単な説明が背後の深い知識の積み重ねの上になされている場合、その背景を知らない読者は分かり難いと考えるのだ。

続きは次回で。
# by tnomura9 | 2012-05-18 18:29 | Haskell | Trackback | Comments(0)
名作コスプレ
初音ミク、巡音ルカ

[Luka & Miku] Happy Synthesizer Dance ver.

初音ミク

【marie】どうしてもミクコスでワールドイズマインを踊ってみたかった!
【りりあ】初音ミク ワールドイズマイン in 豊郷小学校旧校舎
【小倉唯】「みくみくにしてあげる♪【してやんよ】

鏡音リン、レン

【AMU×LUNA】踊ってみた リモコン 【リンレンコス】

メイコ

《complete》-Nostalogic(single edit)-踊ってみた

八九寺真宵

【ゆりにゃ】 ☆化物語☆八九寺のコスプレで帰り道 【踊ってみた】

涼宮ハルヒ

【わた&ささ】最強パレパレード【超会議で踊ってみた】HD
# by tnomura9 | 2012-05-18 04:04 | ボーカロイド | Trackback | Comments(0)
RWH の読み方(44) 第6章
Real World Haskell 第6章 Using Typeclasses

Numeric Types

数の計算(+ などの演算子や sin などの関数)にタイプクラスが必要なのは不思議な感じがするが、ハスケルの扱える数の型の種類が数多いのを見ると必要性が分かる。

ちなみに列挙すると、Double, Float, Int, Int8, Int16, Int32, Int64, Integer, Rational, Word, Word8, Word16, Word32, Word64 がある。それらの詳細はRWHの表に詳しい。

これらの演算が全て同じ + 記号でできるのもタイプクラスのおかげだ。

Numeric クラスの演算子は括弧をつけることによって前置演算子に返ることができる。例えば 1 + 2 は (+) 1 2 と表記することが可能だ。

Numeric クラスの演算子や関数は非常に多いが、RWHに表でまとめてある。Numeric クラスのインスタンスの型は異なる者どうしの演算はできない。変換のための関数が用意してある。例えば Double -> Int は truncate, Int -> Double は fromIntegral などだ。異なる型のデータの演算はこれらで明示的に変換して行わないと型エラーになる。詳細は RWH に詳しい。

Equality, Ordering, and Comparisons

同等性の比較や、順序付けのためのタイプクラスがあり、それぞれ Eq, Ord が標準で定義してある。Eq クラスの演算子は == と /= で、Ord クラスには >=, <= などがある。

Automatic Derivation

deriving キーワードを使って自動的にクラスにインスタンスを登録する方法について書いてある。deriving キーワドの使い方については前回の記事でも実践してみた。全てのユーザ定義のデータが自動登録できるわけではなく、できないものもある。その場合は、手動で演算子や関数を定義しなければならない。

このあたりは、本文を読んでわかりにくいと思うところはない。記事が短くなってしまったが、次の Typeclasses at Work: Making JSON Easier to Use 以下は実践的なタイプクラスの活用法の記事になっているようなので、ここで一区切りとしておく。
# by tnomura9 | 2012-05-16 13:31 | Haskell | Trackback | Comments(0)
RWH の読み方(43) 第6章
Real World Haskell 第6章 Using Typeclasses

Important Built-in Typeclasses

このセクションではいくつかの Haskell Prelude 標準のタイプクラスについて述べてある。タイプクラスの機能はこの言語の特徴の中心的なもののひとつだ。タイプクラスの詳細についてはHaskell ライブラリ・リファレンスが良い情報源だ。

Show

Show タイプクラスは各種データ型の値を文字列に変換する。Show タイプクラスで最も重要な関数は show だ。show の関数の型は次のようになる。

Prelude> :type show
show :: (Show a) => a -> String

すなわち、Show タイプクラスのインスタンスのデータ型の値を引数に取り、それを文字列に変換して戻す。具体的には次のようになる。

Prelude> show 3.14
"3.14"

リストや配列などの型も show 関数を使って文字列に変換できる。

*Main> show [1,2,3]
"[1,2,3]"
*Main> show (1,2)
"(1,2)"

show は文字列に対しても定義されている。

*Main> show "Hello!"
"\"Hello!\""

さまざまな文字列に対する show の振る舞いは RWH を参照してほしい。


show を使って3.14というDouble型のデータを "3.14" という文字列に変換しないと、putStrLn を使って数値をコンソールに印字できない。

Prelude> putStrLn 3.14

:1:9:
No instance for (Fractional String)
arising from the literal `3.14' at :1:9-12
Possible fix: add an instance declaration for (Fractional String)
In the first argument of `putStrLn', namely `3.14'
In the expression: putStrLn 3.14
In the definition of `it': it = putStrLn 3.14

show を使って次のようにすればOKだ。

Prelude> putStrLn (show 3.14)
3.14

ghci に直接値を入力すると、show 関数で文字列に変換してターミナルに出力する。

Prelude> 3.14
3.14

したがって次のようにユーザ定義データ型を data キーワードで宣言しても、

data Color = Red | Green | Blue

Color 型が Show タイプクラスのデータ型でなければ ghci に直接値を入力するとエラーになってしまう。

*Main> Red

:1:0:
No instance for (Show Color)
arising from a use of `print' at :1:0-2
Possible fix: add an instance declaration for (Show Color)
In a stmt of an interactive GHCi command: print it

この場合は次のように Color 型を Show クラスのインスタンスにすればエラーを回避できる。

ファイル名:test.hs

data Color = Red | Green | Blue

instance Show Color where
  show Red = "Red"
  show Green = "Green"
  show Blue = "Blue"

実行例

*Main> Red
Red

もっと簡単には deriving キーワードを使って data キーワードによるデータ型の宣言の時に次のように Color 型を Show タイプクラスのインスタンスにしてしまう。

data Color = Red | Green | Blue deriving (Show)

Read

Read タイプクラスは Show タイプクラスの逆の振る舞いをする。Read タイプクラスの関数 read は文字列を引き数にとって、Read タイプクラスのインスタンスとして含まれる色々な型の値に変換する。read 関数の型は次のようになる。

*Main> :type read
read :: (Read a) => String -> a

つまり文字列を引き数にとって、Read タイプクラス型のインスタンスの型のうちのどれかの値に変換する。どの型に変換するかは型指定演算子で指定する。

*Main> read "3" :: Int
3
*Main> read "3" :: Double
3.0

ユーザ定義のデータ型を Read タイプクラスのインスタンスに登録して、read 関数を定義する手続きはやや複雑になる。それは、read 関数が文字列をパースして型指定演算子で指定された型に変換しなければならないからだ。RWH 本文の例示プログラムの骨格だけを抜き出したものを次に示す。

data Color = Red | Green | Blue deriving (Show)

instance Read Color where
  readsPrec _ value =
    tryParse [("Red", Red), ("Green", Green), ("Blue", Blue)]
    where
      tryParse [] = []
      tryParse ((attempt, result):xs) =
        if (take (length attempt) value) == attempt
          then [(result, drop (length attempt) value)]
          else tryParse xs

これは次のように read として働くが、なぜそうなるのかは分からない。Read タイプクラスの仕様を詳しく調べる必要がある。Read タイプクラスの関数 readPred _ value が何に使われるのかがわかっていないからだ。

*Main> read "Red" :: Color
Red
*Main> read "[Red,Green]" :: [Color]
[Red,Green]

しかし、これも次のように安直に deriving で逃げ切ることができる。

data Color = Red | Green | Blue deriving (Read, Show)

実行例

*Main> read "[Red, Green, Blue]" :: [Color]
[Red,Green,Blue]

Serialization with read and show

メモリ内の構造化されたデータをディスクに文字列として保存することを serialization というが、read と show を使うと簡単に実行できる。次の例は ghci 上でデータの文字列化と文字列からの復元を行なってみたものだ。

*Main> let d1 = [Just 5, Nothing, Nothing, Just 8, Just 9] :: [Maybe Int]
*Main> :type d1
d1 :: [Maybe Int]
*Main> let str1 = show d1
*Main> :type str1
str1 :: String
*Main> let d2 = read str1 :: [Maybe Int]
*Main> :type d2
d2 :: [Maybe Int]
*Main> d1 == d2
True

[Maybe Int] 型のデータ d1 を作成し、show 関数で str1 という文字列に変換しそれを read で再び元のデータ型に復元している。

ファイルへの書き出しや、複雑なデータ型の例が RWH に紹介してある。

まとめ

Show タイプクラスのインスタンスに登録されている型のデータは簡単に文字列に変換できる。また、Read タイプクラスのインスタンスに登録されている型のデータは、文字列から簡単に復元できる。read と show を組み合わせると、構造化されたデータの serialization や文字列からのデータの復元が簡単にできる。
# by tnomura9 | 2012-05-14 23:08 | Haskell | Trackback | Comments(0)
RWH の読み方(42) 第6章
Real World Haskell 第6章 Using Typeclasses

タイプクラス Typeclass とは何か

Real World Haskell の第6章は、Using Typeclasses だ。タイプクラス Typeclass を一言で説明するのは難しいが、例えば左右の値が等しいことを示す同値演算子 == のように、いろいろな型について「等しい」という同じ意味を同じ記号でテストしたい時などに使われる。

いろいろな型が == 演算子で等しいかどうかをテストできるが、それらの型は Eq タイプクラス のメンバーとして登録されている必要がある。タイプクラスとは型の集まりだ。

このような演算子や関数のひとつに show がある。この関数は色々な型のデータを文字列に変換してくれる。この際も、それらの型は Show タイプクラスに登録されていなければならない。

このようなタイプクラスの利点は何かという説明をあまり見たことがないが、ひとつには関数を共有するデータ型をはっきりと把握することで、タイプクラスの関数による期待外の動作を防いでいることがあると思う。

もう一つは、タイプクラスのジェネリックな関数を記述できるので、ユーザ定義型でのタイプクラスの関数の定義が最小限でも多様な関数を使うことができるということだ。

例えば Eq クラスには == という値が等しいかどうかをテストする演算子があるが、Eq タイプクラスの型には、/= という等しくないかどうかをテストする演算子も使うことができる。また、新たにユーザ定義型を作ってそれを Eq クラスに登録する際も、== 演算子の振る舞いだけを記述するだけでそのユーザ定義型に /= 演算子を使えるようになる。

タイプクラスのシステムを使うための手順は、class キーワードによるタイプクラスの宣言と、instance キーワードによるデータ型をタイプクラスに登録して、タイプクラスの関数をそのデータ型に適するようにプログラムするという、やや複雑な手順をとらなくてはならないので、管理人も最初は少し混乱した。

しかし、一旦この手順を理解したら、同じ抽象的な概念であれば、どんなデータ型についても同じ名前の演算子や関数を利用することができるようになる。同じような概念であれば同じ名前の関数でプログラムできるということは、プログラムの記述上も非常に強力な武器を手に入れることになる。少し Haskell に慣れてきたら絶対に習得しなくてはならない知識だ。

また、このタイプクラスを理解することで、ユーザ定義のデータ型を定義するときに必須といえる deriving キーワードを理解することができる。deriving はユーザ定義の型を、タイプクラスに自動的に登録できる非常に便利な機能だ。しかし、これはタイプクラスを理解しておかないと、その意味が分からない。

RWHは第6章で1章を使って、このタイプクラスの解説をしている。第6章に何が書かれているかは、最後の Conclusion に記載してある。直訳になるが次のようになる。
この章では、タイプクラスの必要性とその使い方について学んだ。まず、自分でユーザ定義のタイプクラスを定義した。それから、Haskell の標準ライブラリのなかの重要なタイプクラスについて調べた。最後に、ユーザ定義のデータを自動的にタイプクラスに登録する方法を学んだ。
タイプクラスの説明は、複雑になりやすいが、著者のこの構想を理解してからRWHの本文を読むと分かりやすい。

The Need for Typeclasses

タイプクラスがないと同じデータの「等しさ」を調べるのに、名前の異なる複数の関数が必要になることを示してある。その例として、ユーザ型 Color を定義し、その型の値の等しさをチェックする colorEq を定義している。また、文字列の同等性をチェックする stringEq を定義し、おなじような「等しさ」をチェクする関数が複数あってはわずらわしいこと、できれば、== や /= のような演算子や関数は共通で使いたいこと、タイプクラスはそのための仕組みであることが説明してある。

What are Typeclasses?

型ごとにその値の等しさをチェックする関数 colorEq と stringEq が必要だった不便を、タイプクラスが解消してくれることを示している。まず class キーワードによってユーザ定義のタイプクラス BasicEq を宣言しその関数 isEqual のデータ型を次のように宣言する。

class BasicEq a where
    isEqual :: a -> a -> Bool

次に instance キーワードを使って、Bool 型の isEqual 関数の実体を定義している。

instance BasicEq Bool where
    isEqual True True = True
    isEqual False False = True
    isEqual _ _ = False

そうして、この isEqual が String 型やColor 型にも使えるようにプログラムすることができることを説明してある。

さらに、このセクションでは、BasicEq クラスに isEqual だけでなく、isNotEqual をジェネリックな関数 generic function として定義できることを示している。詳細は本文を参照。

Declaring Typeclass Instances

このセクションでは instance キーワードでタイプクラスのインスタンス(タイプクラスに属するデータ型)を定義できることを示してある。例として Color型 に isEqual が使えるように、これを instance キーワードで BasicEq のインスタンスに登録している。その際に isEqual の Color 型の実体を定義する事になる。

instance BasicEq Color where
    isEqual Red Red = True
    isEqual Green Green = True
    isEqual Blue Blue = True
    isEqual _ _ = False

上のプログラムについて、このセクションの内容とは関係ないが、条件分岐を記述するときのパターンマッチの威力を表している。

次回は、important Built-in Typeclasses セクションで、重要なビルトインのタイプクラスの使い方を調べる。
# by tnomura9 | 2012-05-14 00:26 | Haskell | Trackback | Comments(0)
< 前のページ 次のページ >