ふつケル第3章

型と値

Haskellには型推論という機能がある。型推論は処理系の方で型を推測し、矛盾なく全ての式が型づけできればコンパイル時にエラーにはならない。Haskell以外にも、OCaml、Scala、MLなど静的型付け関数型言語のほとんどが型推論の機能を供えている。 Haskellの型には、Int(整数値)、Char(文字)、String(文字列)、Bool(真偽値)がある。Intは最低30ビット幅の符号付き整数値を意味する。CharやStringで扱う文字(列)のエンコードにはUnicodeを採用しているが、GHCの実装は中途半端で入出力でのエンコーディング変換などが実装されていない。 また、Int型のリスト、Char型のリストと言う風にリストの型を表現し、Int型のリストの各要素はInt型になる。ソースコード上では[Int]や[Char]と書く。文字列は文字のリストなので[Char]ということになり、別名として上記したString型が用意されている。 関数の型をは引数の型と返り値の型を組合せて表現する。例えば、String -> [String]というのは第1引数の型がString(文字列)で、返り値の型が[String](文字列のリスト)となる
第1引数の型 -> 第2引数の型 -> ・・・ -> 返り値の型
では、引数の型が決まっていない場合どうするのかと言うと、型変数と言うものを使う。型変数はアルファベット小文字であらわし、アルファベット小文字の部分は好きな型に読み替えてよい。
[a] -> Int
Int -> [a] -> [a]
上記の場合だと、1行目は任意の型のリストを引数にもち、Intを返す関数。2行目はIntを第1引数、任意の型のリストを第2引数にもち、第2引数と同じ型のリストを返す関数という意味になる。 関数の型を宣言するには、先程関数の型を表現したものに少し付け足せばいい。Haskellは型推論の機能をもつので必ずしも型の宣言が必要というわけではないが、なるべく宣言しておくのが好ましい。
関数名 :: 第1引数の型 -> 第2引数の型 -> ・・・ -> 返り値の型

高階関数

高階関数とは引数に関数をとる関数のこと。
main = do cs < - getContents
          putStr $ expand cs

expand :: String -> string
expand cs = map translate cs

translate :: Char -> Char
translate c = if c == '\t' then '@' else c
このプログラムでは5行目で高階関数のmap関数が使われている。map translate csと言うのは、translate関数そのものにmapを適用しているのであって、translateの結果にmapを適用しているわけではない。 mapの型は
map :: (a -> b) -> [a] -> [b]
となっており、(a -> b)は「a型の値を引数にとりb型の値を返す関数」を意味する。今回の場合だと、translate関数の型が Char -> Char となっているので、「Char型の値を引数にとりChar型の値を返す関数」と読み替えることができる。更にいうと(a -> b) -> [a] -> [b] は (Char -> Char) -> [Char] -> [Char]となる。
main = do cs < - getContents
          putStr $ expand cs

expand :: String -> String
expand cs = concat $ map expandTab cs

expandTab :: Char -> String
expandTab c = if c == '\t' then "        " else [c]
この辺はちょっとややこしくなってくるが、ちゃんと順に考えていけば問題ない。

パターンマッチ(1)

tabStop = 8

main = do cs < - getContents
          putStr $ expand cs

expand :: String -> String
expand cs = concatMap expandTab cs

expandTab :: Char -> String
expandTab '\t' = replicate tabStop ' '
expandTab c = [c]

個人的にtabStopがグローバル変数みたいでなんか気持ち悪い。定数と考えればいいのかな。まぁ今はとりあえず置いておこう。

今回のプログラムの最後2行でパターンマッチを使用している。下から2行目が引数が「\t」の時に使われる定義。最後の行は仮引数が使われているので、「どんな値」にもマッチする。パターンマッチは上に書いたものが優先され、マッチするパターンが無い場合には実行時にエラーが発生する

関数名 第1引数のパターン 第2引数のパターン ・・・・ = 定義1
関数名 第1引数のパターン 第2引数のパターン ・・・・ = 定義2
関数名 第1引数のパターン 第2引数のパターン ・・・・ = 定義3

パターンマッチ(2)

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = f x : map f xs

2行目のパターンマッチは空リストに一致する。つまりmapの第2引数が[]だった場合、必ず[]を返すと言うことになる。

3行目の(x:xs)もリストに対するパターンで、空リスト以外のリストにマッチする。リストの最初の要素がxに束縛され、残りの要素がxsに束縛される。まぁSchemeのcarやcdrみたいなものかな。

3行目での定義ではまたさらにmapを適用している。つまり、再帰を用いて処理している。Haskellにはループを扱う構文は存在しない為である。

map length ["abc", "de", "f"]
  length "abc" : map length ["de", "f"]
    length "de" : map length ["f"]
      length "f" : map length []
      (1 : [])
    (2 : [1])
  (3:[2,1])
[3, 2, 1]

例えば「map length [“abc”, “de”, “f”」だと多分こんな感じ。「:」演算子はリストを生成する演算子で(y:ys)ならリストysの先頭に要素yを追加する

練習問題

main = do cs < - getContents
          putStr $ swapa cs

swapa :: String -> String swapa cs = map replaceA cs

replaceA :: Char -> Char replaceA ‘a’ = ‘A’ replaceA ‘A’ = ‘a’ replaceA c = c

とりあえず動く。

追記(08/02/27)

main = do cs < - getContents
          putStr $ map swapa cs

swapa :: Char -> Char swapa ‘a’ = ‘A’ swapa ‘A’ = ‘a’ swapa c = c

書き直した。この方が短い。