Skip to content
This repository has been archived by the owner on Nov 11, 2021. It is now read-only.

yaakaito のためのHaskellコードの読み方

VoQn edited this page May 21, 2012 · 27 revisions

どの道実装の為にオリジナルのQuickCheckのソースを読むことになるので、ざっくりとした Haskell のコードの読み方を書く

ライブラリの読み方 も合わせて読もう

##「…これ、何???」 hoogle 行って調べる

##リテラル/トークン

  • 大文字で始まるものは モジュール型クラス 、(任意型以外の) データコンストラクタ
  • 小文字で始まるものは 予約語関数任意型
  • 数字だけなのは 数値リテラル (割と特殊な振る舞いをするけど、このページの趣旨は読み方なので省略)
  • アルファベット+数字,以外の ASCII 文字列だけで構成されているのは 演算子
  • () と [] と {} は文脈によって複数の意味を持つで後述

Haskell の予約語

##ちゃんと知りたい Learn You a Haskell for Great Good!

###「オゥ、ワタシ、エイゴ、ワカリマセン」 すごいHaskellたのしく学ぼう

##関数宣言 Haskell には変数もオブジェクトも無く、あるのは関数のみ ###再代入禁止

x = 10 -- 名前つき関数xは呼び出した結果 数値型(Num a) 10 を返す
x = 20 -- 再代入はできない。コンパイルエラーとなる

###型シグネチャ

y :: Int -- (関数名)::(型) で 「この関数はこの型の値を返すよー」 という宣言
y = 10 -- 上の x の例と異なり、 y の返す 10 は Int型として強く制限される

add2 :: Int -> Int -- (関数名)::(型1)->(型2) で「この関数は型1の引数から型2の返り値返すよー」という宣言
add2 x = x + 2
-- 引数は省略できる
add3 = (+) 3
-- 演算子は()でくくると、通常の関数のように使える

###関数適用(呼び出し)

-- add2 も add3 も呼び出し方はかわらない
z = add2 5 -- z は 7
add4 = add2 . add2 -- (関数) . (関数) は 関数(関数(省略された引数)) と同じ
add12 = (+ 10) . add2 -- 「(書いてないけど引数を)add2 して、 (+ 10) するよー」

###部分関数宣言とプレースホルダ,パターンガード

is3 :: Int -> Bool
is3 3 = True -- このように引数が特定の場合の宣言が出来る
is3 _ = False -- 引数に関係なく返せる場合、 _ で 「引数は取るものの、この場合は利用しないわ」 と表現できる

is4 :: Int -> Bool
is4 x
 | x == 4 = True -- | (引数の特定の条件) = (返り値)
 | otherwise = False -- | otherwise = (返り値) で、どのパターンに適合しない場合の戻りを指定

###内部関数とラムダと let

-- 以下2つ同じモノを返す実装
sq0 x = x * x
sq1 = \x -> x * x -- ラムダ式、 \(引数) (引数) ... -> (返り値) で無名関数となる

fizzbuzz x
  | isMultipleOf x 15 = "FizzBuzz" -- isMultipleOf は今まで名前空間に宣言されてない。
  | isMultipleOf x 5  = "Fizz"
  | isMultipleOf x 3  = "Buzz"
  | otherwise = let x' = show x in x' -- let (局所変数名) = (値) in (返り値) で in 以降で使えるスコープの変数を用意できる
  where -- この時の where は fizzbuzz の関数内のスコープに限定した関数を宣言できる
  isMultipleOf x n = 0 == mod x n

##演算子 演算子は、「2つ引数を取り、1つの返り値を返す関数」の表現でしかない。

  • 演算子は()でくくると、通常の関数のように使える
  • 普通の関数をバッククオートで囲むと、演算子のように使うことができる。
func x y = x + y
a = x `func` y 

(%) = mod
-- 5 % 3
-- (%) 5 3
-- 5 `mod` 3
-- mod 5 3

##型 そもそも型ってなんだ。

型は関数が返す値を分類する指標とも、(そもそも全部が関数なんだから)関数を分類する指標とも言える。

Haskell では型を指定しない関数の宣言に対して、実装で適用されている関数の関係から、自動的に「まぁ、こんなもんかな…」と推測してコンパイルする

###代数的データ型

data RGB = MakeRGB Int Int Int -- 代数的データ型 と呼ばれるパターン
-- 3つの Int 型から新しい型 RGB を宣言する。
-- この宣言にある RGB は 「型コンストラクタ」と呼ぶ
-- この宣言にある MakeRGB は 「データコンストラクタ」と呼ばれる「関数」になる。
-- data (型コンストラクタ) = (データコンストラクタ) (メンバ)

colorRed :: RGB
colorRed = MakeRGB 255 0 0 -- このようにデータコンストラクタを利用する

data Vector2D = Vector2D{ xPos :: Double, yPos :: Double }
-- データコンストラクタの名前は、型コンストラクタと同じ名前でも良い
-- メンバに対するアクセサを {(アクセサの名前)::(型)} で宣言できる

scalar :: Vector2D -> Double
scalar (Vector2D x y) = sqrt $ x^2 + y^2

###列挙型 データコンストラクタは複数列挙することができる。特に引数が無いコンストラクタだけが列挙されたパターンを列挙型と呼んだりする。

data NamedColor = Red | Green | Blue
-- 列挙型は | でコンストラクタを区切る

data Maybe a = Nothing | Just a
-- 代数的データ型も列挙できる。

###型シノニム(型の別名) 既存の型の別名を名付けられる。

例えば、単に文字列型を意味する String を、人名を意味する型として ParsonName と明示することができる。

そもそも、 String も 文字型 Char のリスト [Char] の型シノニムとして実装されている

type Name = String -- 型シノニムは type (別名) = (オリジナルの型) と宣言する

colorName :: NamedColor -> Name -- 型シノニムを使わない場合、NamedColor -> String となる
colorName c = case c of
  Red -> ""
  Green -> ""
  Blue  -> ""

##型クラス 型クラスは、OOP における インターフェースに近い機構。厳密には異なる。 型クラスは、関数の型に対して「コイツは同じ型同士なら…

  • 足し算できる
  • 引き算できる
  • 掛け算できる
  • 正負反転できる

…」などと明示する。

add :: Num a => a -> a -> a -- Num型クラスのインスタンスならなんでも良いので、同じ型aを2つ引数にとって、同じ型を返すよーという宣言
add = (+)

upCastInt :: Num a => Int -> a -- Int型をアップキャストするよ宣言
upCastInt = fromIntegral

###型クラス宣言の例

class Color a where -- 型クラス宣言、「型クラスColorのインスタンスを a とするなら…」
  showHexRGBExpr :: a -> String -- RGBのHex表現が出来るはずだよね
  toRGB :: a -> RGB -- RGB 色空間の座標に出来るはずだよね
  toHSL :: a -> HSL -- HSL 色空間の座標に出来るはずだよね
  htmlColor :: a -> Maybe String -- もし HTML で使える名前指定の色だったら、その色の名前を返せるよね

###型クラスのインスタンス宣言

data RGB = MakeRGB Int Int Int -- Int 3つで RGB の色を表現する型を宣言

instance Color RGB where -- 型RGB は Color型クラスのインスタンスだよー
  showHexRGBExpr (MakeRGB r g b) = "#" ++ hexExpr r ++ hexExpr g ++ hexExpr b -- こう実装したよー
      where hexExpr x = showHex x ""
  toRGB rgb = rgb -- そもそも RGB 型なのでそのまま返せばよい
  toHSL rgb = -- ... 以下同じ様に実装を書いていく
  -- ...

##任意型 もうすでに前段で何度か出てる a や b などで指示されている型について。

###乱暴な表現で言えば「なんでもよい」

id :: a -> a
id x = x -- もらった引数をそのまま返す。これであれば型がどうとか関係ないよね

seq :: a -> b -> b
seq x y = y -- もらった引数の2つ目をそのまま返す。これも型がどうとか関係なく宣言できる

###undefined undefined任意型 a の関数 なのでどこでも使えるけれど、 undefined が評価された時、 例外が発生する

本当に 仕様上、あるいは学術上未定義値にならざるを得ない場合 などに使われる。

あと、「実装思いつかないんだけど、コンパイルは通したい] みたいな開発の最中で一時的に使うことはある。

main = do
  result <- func . getLine -- 標準出力受け取って、なんかする
  return $ putStrLn $ show result -- なんかした結果を出力する

func :: a -> b
func = undefined -- 思いつかないけど型が合ってるかだけ確認したいから undefined にしとく

他に見かける場面は 条件分岐の際に、「私のせいじゃないですぅ~~ 使ってる他のライブラリが悪いんですぅ~~ ほんとですぅ~~」っていうアピールとか

###型クラスレベルの型コントロール ただし、

  • 「型クラス Eq のインスタンスであればなんでも良い」
  • 「型クラス Monad のインスタンスであればなんでもよい」

などの、 型クラスレベルで適用範囲をコントロールできる のがミソ

sq :: Num a => a -> a -- (Num 型クラスのインスタンスを抽象的に仮に a と呼んでおくよ) => a -> a
sq x = x * x

example1 :: Int
example1 = 10

example2 :: Double
example2 = 10.5

sqEx1 = sq example1 -- Int型 は Num型クラスのインスタンスなので使える
sqEx2 = sq example2 -- Double型 も Num型クラスのインスタンスなので使える

errCase = example1 + example2 -- これはコンパイルエラー、 (+) は Num型クラスでも 同じ型同士で出来る演算
safeCase =
  let exp1' = fromIntegral example1 -- fromIntegral は Num a として曖昧なアップキャストをする
  in exp1' + example2  -- exp1' が ( + (Doubleの値) ) から型推論でDoubleとみなして計算してくれる

###代数的データ型の引数の型として使える

data Maybe a = Nothing | Just a -- Maybe a は 何も無いことを示す Nothing か Just a だよ

find :: Eq a => a -> [a] -> Maybe a -- 「リストから、1つめの引数と同じものを取ってくる、たぶん」
find _ [] = Nothing -- リストが空っぽだったから、見つけらんなかったよ
find x (y:ys) = if x == y -- 2つ目の引数のリストの先頭と比較して…
  then Just x -- 「同じのあったよー」
  else find x ys -- 「同じじゃなかったから、残りのリストで調べるの続けるよー」

##そのほか、特殊な型 任意型の説明が出来たので、他の特殊な型について ###Void () は 他の言語でいう void 。返り値が無い

main :: IO () -- I/O なので、外部との入出力を行って、結果は何も返さない
main = putStrLn "Hello, World!"

###Tuple (a,b) などで表現される。 Tuple タプル と読む。

型が違う幾つかをまとめて、データ構造を一時的に用意するのに便利。よく一対の組を取り扱う際に利用される。 ####すごいソースコード GHC.Tuple

data (,) a b = (,) a b
data (,,) a b c = (,,) a b c
data (,,,) a b c d = (,,,) a b c d
data (,,,,) a b c d e = (,,,,) a b c d e
data (,,,,,) a b c d e f = (,,,,,) a b c d e f
data (,,,,,,) a b c d e f g = (,,,,,,) a b c d e f g
-- 以下略

####例

setPair :: a -> b -> (a, b)
setPair x y = (x, y)

first :: (a, b) -> a
first (x, _) = x

second :: (a, b) -> b
second (_, y) = y

set5 :: a -> b -> c -> d -> e -> (a, b, c, d, e) -- このようなタプル型もできる
set5 x y z w v = (x,y,z,w,v)

###List [a] で示す。 ####空のリスト [] で表現される。 ####要素が1つだけのリスト (x:[])[x] で表現される ####それよりも要素が多いリスト x:xs で、 「リストである xs の先頭に x を追加する」 意味を持つ。

x:y:xs とも、 x:y:z:[] とも、 x:y:[z] とも書ける

####例

car :: [a] -> a -- リストから先頭を返す
car [] = undefined -- 空リストからは先頭は取れないので未定義。この実装でこのパターンに入るとランタイムエラーとなる
car (x:xs) = x

cdr :: [a] -> [a] -- リストから先頭以外を返す
cdr [] = []
cdr (x:xs) = xs

###(->) haskell は 全てが関数であるという ピュアな気持ち を大事にしているので 「任意型 a を引数にとり 任意型 b を返り値に返す関数」 が代数的データ型で宣言されている。

data (->) a b = (->) a b -- 「a を受け取って b を返すデータ型」だって言えるよね

addNumber :: Number a => (->) a b -- こう書いてもよい
sumOf3 :: Number a => (->) ((->) a b) c -- 当然こう書いたって良い
reduceWith :: Numbar a => (a -> b) -> [a] -> b -- こういうのもアリ
-- この時 (a -> b) で一括りの型で、ラムダや引数が1つ必要な関数を渡す

##「(),[],{} の使い方、なんかフリーダムじゃね?」 書き途中…