第2回 ふつうのHaskellプログラミング読書会

ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門ふつうのHaskellプログラミングの3~4章を読みました。
以下、復習です。

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

Haskellの型
  1. Haskellは静的な型チェック(コンパイル時に処理系が型をチェックする)
  2. 「型推論」という機能で、引数や返り値の型を宣言しなくても処理系がその型を推測してくれる
  3. 型推論の結果、矛盾なく全ての式が型付けできればエラーにならない(型宣言は必須ではない)
型の宣言をしなくても良いが。。。
  • 関数の型を宣言するのは良い習慣
  • 意図したとおりに型がチェックされることが確実になる
  • ソースコードも読みやすくなる
  • 型変数を大量に使うようなコードでは、型推論が失敗することもあるので、その場合はコンパイルエラーになる
基本型

Int、Char、String、Bool(True、False)、[XX](リスト)

リストは、「**型のリスト」の様に表現する。

Prelude> :t ['a', 'b', 'c']
['a', 'b', 'c'] :: [Char]   -- Char型のリスト

Prelude> :t "abc"
"abc" :: [Char]  -- 文字列Stringは、[Char](Charのリスト)
関数の型

関数の型は、引数・返り値の型の組み合わせで表現する。

関数名 :: 引数 -> 返り値
関数名 :: 引数1 -> 引数2 -> ... 引数n -> 返り値

lines関数は、引数に文字列をとり返り値が文字列のリストになるので、以下の様になる。

-- lines関数
Prelude> lines "abc"
["abc"]
Prelude> lines "a\nb\nc"
["a","b","c"]

-- lines関数の型宣言
Prelude> :t lines
lines :: String -> [String]

複数の引数がある関数の場合には、最後の型が返り値で、それ以前の型は引数となる。

-- replicate関数(第2引数の値を第1引数の数含むリストを返す)
Prelude> replicate 3 'a'
"aaa"

-- replicate関数の型宣言
Prelude> :t replicate
replicate :: Int -> a -> [a]

replicate関数のa[a]の様な表記は型変数と言い、どんな型と置き換えても良いことを意味する。→多相型(polymorphic type)


replicate関数は第1引数にInt、第2引数に**型を与えれば、**のリスト([**])を返す。

Prelude> replicate 3 "abc"
["abc","abc","abc"]
Prelude> replicate 4 'a'
"aaaa"
Prelude> replicate 3 5
[5,5,5]
Prelude> replicate 2 ["abc", "def", "ghi"]
[["abc","def","ghi"],["abc","def","ghi"]]
高階関数

まず、関数について
関数を定義する = 関数名を関数に束縛する
関数名は変数であって、その変数は関数に束縛されている。

以下は、「nを2乗する関数」に変数squareが束縛されている。

square :: Int -> Int
square n = n * n


JavaScriptでも、そのまま以下の様に書ける。

var square = function (n) { return n * n;};


関数を定義する(と思っていた)時の関数名は、つまり変数なので、他の関数の引数として渡すことができる。
で、高階関数とは、他の関数を引数にとる関数のことを言う。

map関数
Prelude> :t map
map :: (a -> b) -> [a] -> [b]

map関数は、第2引数のリストの各要素に対して第1引数を適用したリストを返す。
第1引数の型(a -> b)は、「a型の引数をとり、b型の値を返す関数」を表す。

square :: Int -> Int
square n = n * n

map square [1, 2, 3]
→[1, 4, 9]
if式
if 条件式 then1 else2

条件式がTrueなら式1、Falseなら式2を評価する。

  • elseは省略不可
  • 条件式の値はTrueかFalseである必要がある。(整数値や文字列はだめ)
==

「==」は二項演算子のように使えるが、実は関数。

-- (==)関数の型宣言
-- 二項演算子の様に使える関数は、()でくくったものが関数名になる。
(==) :: a -> a -> Bool
Prelude> 'a' == 'a'
True
Prelude> 'a' == 'b'
False
Prelude> 1 == 2
False
Prelude> 1 == 1
True

パターンマッチ

関数がとる引数の値のパターンによって、場合分けをすることができる。以下のswapa関数は、引数として'a'が来たら'A'、'A'が来たら'a'、それ以外の文字ならそれをそのまま返す。
マッチするパターンが無いと実行時エラーになるので注意する必要がある。

swapa :: Char -> Char
swapa 'a' = 'A'
swapa 'A' = 'a'
swapa c = c

map関数の実装

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


まず、パターンマッチで、第2引数が空リストならそのまま空リストを返す。

Prelude> map square []
[]


空リストでなければ>||map f (x:xs) = f x : map f xs||<にマッチする。
「:」はリストを生成する関数で(x:xs)は、リストxsに値xを先頭に追加したリストを作る。

Prelude> 'a' : ['b', 'c']
"abc"
Prelude> 1 : [2 , 3]
[1,2,3]


つまり「(x:xs)」は、引数のリストをリストの先頭の要素をx、先頭以降の要素から成るリストxsに分解して
関数の値"f x : map f xs"にそれぞれ適用する。リストxsに対しては、さらにmap関数を適用しているので再帰。
最終的に、引数のリストの全要素に対し、引数として与えられた関数を適用した値を":"でリストにして、そのリストを返すことになる。

4.モジュール

Haskellのプログラムは、関数や変数、型の集まりであるモジュール単位で分割されている。
他のモジュールで定義されている関数を使いたい場合には、モジュールをインポートする。

import System


main変数が属するMainモジュールは省略可能で、省略した場合には暗黙的にそのファイル全体がMainモジュールになり、そのmain変数だけが外部に公開される。
また、基本的な型や関数が定義されているPreludeというモジュールは、暗黙的にインポートされているので宣言する必要は無い。


で、モジュール一覧はどこで見れるのか、と探してみたらありました。

Basic libraries
http://www.haskell.org/ghc/docs/6.12.2/html/libraries/base-4.2.0.1/index.html