SE情報技術研究会’s blog

http://se-info-tech.connpass.com

2015-12-21 『ふつうのHaskellプログラミング』 もくもく読書会(第3章) の振り返り

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門

第3章 Haskellの基礎(2) 型と高階関数

  • Haskellでは静的な型チェックを行う
    コンパイル時に型のチェックを行う
  • Haskellでは引数や戻り値の型を明示的に書かなくても推論してくれる

関数の型

  • 引数の型と戻り値の型の組み合わせで関数の型を表現する
  • 引数と引数の間および引数と戻り値の間に->を入れる
  • 関数名と関数の型の間に::を入れる
  • 関数の型は宣言しなくても型推論によって使えることもあるが、型宣言をすることで意図したとおりの型かチェックできる
関数名::引数1の型 -> 引数2の型 ->-> 戻り値の型

例:lines関数の場合

lines :: String -> [String]

これは引数にStringを受け取り戻り値にStringのリストを返す関数の型をあらわす。

型変数

  • リストの要素のように型を特定しないときに使う変数
  • 型変数を含む型のことを多相型(polymorphic type)と言う
  • 型変数はアルファベットの小文字から始まる識別子なら何でも使える
  • 慣例的にa,b,c…など1文字が型変数に使われる
  • 1つの式に複数の多相型を持つ場合、それが同じ型なら同じ型変数、違う場合なら違う型変数を使う

例:同じ型変数の場合(take関数)

take :: Int -> [a] -> [a]

例:違う型変数の場合(map関数)

map :: (a -> b) -> [a] -> [b]

高階関数

  • 関数を引数に取る関数

値としての関数

  • 関数は値として使われる
  • 関数名は変数
    → squareを「nを2乗する関数」の変数と見ることもできる

例:

square n = n * n
  • 引数や戻り値で「a型の値からb型の値を返す関数」をあらわす場合、(a -> b)と記述する

map関数

  • リストの各要素に対し引数の関数を適用する関数
  • Haskellではよく使われる関数の一つ

例:[1,2,3]がどのように変わるのか段階的に見ていく

map square [1,2,3]

[(square 1), (square 2), (square 3)]

[1,4,9]

expand.hs

  • 入力内容のタブ文字を空白文字に展開するコマンド
  • map関数を使う

バージョン0

まずはタブ文字を「@」に変換するようにするコマンドを作成する

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

translate :: Char -> Char
translate c = if c =='\t' then '@' else c

Windowsの場合バックスラッシュは円マークになる

  • expand関数は引数のcsをtranslate関数に適用する
  • csは入力されたStringでStringは[Char]である
  • translate関数は引数の文字cをif式で判定し対象の値を返す
if式
if 条件式 then1 else2
  • 式1と式2はこの例では値
  • if式全体が値となる
  • Haskellではifは文ではなく式なのでelseを省略できない

バージョン1

  • この段階ではタブ文字を空白8文字に置き換えるようにする
  • expandTab関数で文字から文字列に変換しているのでconncat関数を使ってネストした文字列を外している
…略
expand cs = concat $ map expandTab cs

expandTab :: Char -> String
expandTab c = if c =='\t' then "        " else [c]

AL\tAlabama…と言う文字列がmap expandTab csで下記のようなリストになる。また\tは空白8文字の文字列だが、これは空白文字が8つのリストと同じ。

[['A'], ['L'], [' ', ' ', …],…, ['m'], ['a']]

↓ これをconcat関数を使って中のリストの囲いを外す

['A', 'L', ' ', ' ', …, …, 'm', 'a']

"AL      Alabama"
concat関数
concat::[[a]] -> [a]
  • リスト内にあるリストの囲いを外す関数
  • 外す囲いは1つのみ

パターンマッチ

  • 関数に引数のパターンを指定して処理を定義する方法
  • パターンマッチに該当しないものがある場合はエラー

バージョン2の例より:

expandTab :: Char -> String
expandTab '\t' = 処理1
expandTab c    = 処理2

この場合、タブ文字だと処理1をそれ以外だと処理2を行う

バージョン2

変更箇所は下記

concat関数の個所

concat $ map expandTab cs

concatMap expandTab cs

expandTab関数の個所

expandTab c = if c =='\t' then "        " else [c]

expandTab '\t' = replicate tabStop ' '
expandTab c    = [c]

まずconcat $ map expandTab csと2つの関数を使っていた箇所をconcatMap expandTab csと標準で用意されているconcat関数とmap関数を1つにまとめた関数を使うように変更。

expendTab関数についてはif式で条件分岐していたものをパターンマッチに変更

また、" "と直接空白文字を埋めていた箇所をreplicate tabStop ' 'と変更
※ tabStop = 8

replicate関数
replicate :: Int -> a -> [a]

replicate関数は第1引数にIntを取り第2引数の値を第1引個のリストを作成する関数

replicate tabStop ' 'は空白文字8個のリスト、すなわち8個の空白の文字列になる

リストのmap関数の定義について

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

パターンマッチ

map関数では第2引数に対してパターンマッチを行っている

  • [] は空のリストの場合は空のリストを返す。
  • (x:xs)は要素を持つリストのパターンでxheadxstailをあらわす
  • head = リストの最初の要素
  • tail = リストの最初の要素を除いた全てのリスト(headのみの要素しか持たないリストはtailは空リストになる)

「:」演算子

要素 : リスト

で最初の要素に指定した要素を持ち残りが指定したリストのリストを作成する演算子

Haskellではリストを

head : tail

として定義することができる

f x : map f xs

headに関数fを適用したxの値、tailにmap関数を適用して第2引数に対象のリストのtail部分を渡して、残りの要素も同様に関数fが適用されるようにしている

再帰

  • map関数のように自分自身の処理で自分自身の関数を呼び出すことを再帰と言う

  • Haskellではforループのようなループの構文がないので再帰を使って繰り返し処理を行う

練習問題

小文字を大文字に変更する練習問題かと思ったが、答えを見たら'a'から'A'と'A'から'a'だけ変換する問題だった。 この章の目的だと、パターンマッチとmap関数を使って問題を解くべきなのだろう。

とりあえず、問題の意図をくんでなかったけど、小文字から大文字に変換するパターンで答えた例:

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

swapa :: Char -> Char
swapa 'a' = 'A'
swapa 'b' = 'B'
swapa 'c' = 'C'
swapa 'd' = 'D'
swapa 'e' = 'E'
swapa 'f' = 'F'
swapa 'g' = 'G'
swapa 'h' = 'H'
swapa 'i' = 'I'
swapa 'j' = 'J'
swapa 'k' = 'K'
swapa 'l' = 'L'
swapa 'm' = 'M'
swapa 'n' = 'N'
swapa 'o' = 'O'
swapa 'p' = 'P'
swapa 'q' = 'Q'
swapa 'r' = 'R'
swapa 's' = 'S'
swapa 't' = 'T'
swapa 'u' = 'U'
swapa 'v' = 'V'
swapa 'w' = 'W'
swapa 'x' = 'X'
swapa 'y' = 'Y'
swapa 'z' = 'Z'
swapa c   = c

単純にパターンマッチで小文字の全アルファベットに対応させただけ。

実際の本の解答も似たようなものだった。