ホーム‎ > ‎Haskell入門‎ > ‎

型と関数

型について

Haskellの変数や関数には、必ず「型」がついています。大雑把な説明をすると、型とは、変数が持てる値の「種類」のようなものです。例えば、Integer型を持つ変数には必ず整数が入っていますし、String型を受け取る関数には文字列しか渡せません。変数や関数に型をつけることによって、プログラムの間違いをある程度検出することができます。

これまでに出てきたものを中心に、いくつか、代表的な型を挙げてみます。

  • 整数 Integer
    • 任意の桁数の整数を扱える。
  • 固定長整数 Int
    • 有限桁(典型的には、32ビットや64ビット)の整数しか扱えないが、効率がいい。
  • 有理数 Rational (Data.Ratioモジュール)
  • 倍精度浮動小数点数 Double
  • 文字 Char
  • 文字列 String
    • String[Char]は同じ型です。
  • 真理値 BoolTrue/False
  • リスト [a]aは要素の型)
  • 関数型 a -> b

型がついていることの結果として、1つのリストに型が異なる値を入れることはできません。

[123,"Hello"] -- エラー

値が持つ型を明示するには、値 :: 型という構文を使います。
Prelude> 123 :: Integer
123

対話環境で :t コマンドを使うと、値が持つ型を調べることができます。

Prelude> :t "Hello"
"Hello" :: [Char]
Prelude> :t True
True :: Bool

関数の型

さて、関数型のところに a -> b と書きましたが、Haskellの関数は必ず1つの引数を受け取って1つの値を返します。複数の引数を取る関数はどうなってるのかというと、戻り値 b の型がさらに関数型になっています。

例として、take関数を見てみましょう。take関数はリストの最初のn項をリストとして返す関数でした。

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

takeの型をカッコを省略しないで書けば、 Int -> ([a] -> [a]) となります。つまり、takeInt を受け取って [a] -> [a] という型の関数を返す関数です。take n [1,2,3]という式をカッコを省略しないで書けば (take n) [1,2,3] となって、take n が返す値にさらに関数適用しているのです。

ということなのですが、関数型の -> は右結合、関数適用は左結合なので、あたかも複数の引数を受け取る関数を扱えるように見えるわけです。

map関数の型を見てみましょう。

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

map関数は a -> b 型の関数とリスト [a] を受け取って、リスト [b] を返す。

問:filter関数の型を調べてみましょう。

Haskellにある程度慣れてくると、関数の型を見ることで関数の仕様がだいたい推測できます。Haskellコミュニティーには、型によって関数を検索する検索エンジン Hoogle があります。

問:Hoogleで、型 a -> b -> a をもつ関数を探してみましょう。

演算子について

これまでいろいろな演算子を見てきましたが、これらの2項演算子は、名前が変なことを除けば、ただの2変数関数です。

Haskellでは、2項演算子をカッコでくくると、2変数関数として扱えます。

Prelude> (+) 2 3       -- 2 + 3 と等価
5
Prelude> (:) 1 [2,3,4] -- 1:[2,3,4] と等価

[1,2,3,4]

逆に、2変数関数の名前をバッククォート ` でくくると、関数を中置演算子として扱えます。

この機能が便利な例として、mod関数があります。mod関数は割り算の余りを求める関数です。

Prelude> mod 10 3 -- 普通に使う
1
Prelude> 10 `mod` 3 -- 中置記法
1

2項演算子の片方だけに値を代入して、新しい関数を作ることができます。いくつか例を見てみましょう。

  • (+ 3) -- 与えられた数に3を加える関数。\x -> x + 3 と等価。
  • (`mod` 3) -- 与えられた整数を3で割った余りを返す関数。

  • ("Hello, " ++) -- 与えられた文字列の前に"Hello, "を付け加える関数。\x -> "Hello, " ++ x と等価。

  • (- 5) -- 与えられた数から5を引く関数。…にはならない!これはただのマイナス5という数になる。「与えられた数から5を引く関数」はsubtract 5と書く。

実行例:

Prelude> (+ 3) 5
8
Prelude> map (`mod` 3) [10,20,30] -- map関数で使ってみる
[1,2,0]
Prelude> ("Hello, " ++) "World!"
"Hello, World!"
Prelude> map (subtract 5) [10,20,30]
[5,15,25]

ところで、これまでにたくさんの演算子が出てきました。他のプログラミング言語だと四則演算、ビット演算、比較、ぐらいしか演算子が用意されていないこともありますが、Haskellではここまでの例で既に、リストに関するもの (:), (!!), (++)、有理数を作るもの (%) が出てきました。この後にも、関数合成や関数適用の演算子 (.), ($) が出てきます。これはちょっと普通のプログラミング言語と比べると多すぎではないでしょうか。

実は、Haskellの文法的には、1文字以上の記号の列はだいたい演算子として扱われます。なので、1文字以上の記号の列に、演算子の優先順位および関数としての定義を与えてやれば、その記号列を2項演算子として使うことができるのです。これまでに出てきた演算子も、言語組み込みというよりは、標準ライブラリで定義されたものです。

関数について

ラムダ式

Haskellでは関数自体を式として書くことができます。ここまで読んできた方は、すでに何回か見ているでしょう。

文法としては、(バックスラッシュ) (引数の名前) (右矢印) (関数の定義) という順番になります。

例: \n -> n + 1

使用例:

Prelude> map (\n -> n + 1) [1,2,3]
[2,3,4]

複数の引数を受け取ることもできます。

例: \x y z -> sqrt (x^2 + y^2 + z^2)

使用例:

Prelude> (\x y z -> sqrt (x^2 + y^2 + z^2)) 2 3 4
5.385164807134504

関数定義

今度は、名前の付いた関数を定義する文法を見てみましょう。ここまでの例では、基本的に対話環境に打ち込むことを想定してきましたが、このセクションではプログラムをファイルに書くことを想定します。

名前の付いた関数を定義する文法は、大雑把に言うと、(関数名) (引数) (イコール) (関数の定義) となります。例を見てみましょう。

succ n = n + 1

関数の型を明示するには、関数名に対してコロン2つを使います。型を明示しなくても、Haskell処理系は定義から型を推論してくれますが、あえて型を書いておくことで、定義を間違えた場合に処理系に(コンパイルエラーによって)教えてもらえます。

succ :: Integer -> Integer
succ n = n + 1

引数名を書かず、代わりにラムダ式で定義することもできます。

succ :: Integer -> Integer
succ = \n -> n + 1

引数を取らない関数も定義できます。いわゆる「定数」です。

numberOfMyFriends :: Integer
numberOfMyFriends = 0

複数の引数を取る関数も書けます。

aisatsu :: String -> String -> String
aisatsu me other = "Domo, " ++ other ++ "-san. I'm " ++ me ++ "."

ガード

縦棒を使うと、「場合分け」によって関数を定義することができます。文法は (関数名) (引数) (縦棒) (条件) (イコール) (関数の定義) となります。例を見てみましょう。

doubleFact :: Integer -> Integer
doubleFact n | n <= 1 = 1
doubleFact n = n * doubleFact (n - 2)

上の doubleFact の定義では、n <= 1 の場合は 1 が、そうでない場合は下に書いた n * doubleFact (n - 2) が定義として使われます。縦棒で書いた条件のことを、「ガード」と呼びます。

縦棒を縦に並べ、条件と定義の組を複数書くこともできます。条件のところに True と書けば、ガードを書かないのと同じになります。

doubleFact n | n == 0 = 1
             | n == 1 = 1
             | True   = n * doubleFact (n - 2)

ガードに True と書いてもぶっちゃけ嬉しくないのですが、標準ライブラリで otherwise という定数が otherwise = True と定義されているので、場合分けを次のように自然に書くことができます。

doubleFact n | n == 0    = 1
             | n == 1    = 1
             | otherwise = n * doubleFact (n - 2)

パターンマッチの初歩

関数の定義のところで、引数の名前の代わりに値を直接書いて場合分けするとこができます。

doubleFact 0 = 1
doubleFact 1 = 1
doubleFact n = n * doubleFact (n - 2)

さっきのaisatsu関数をいじって、相手がyakuzaの場合に威嚇するようにしてみましょう。

aisatsu :: String -> String -> String
aisatsu me "yakuza" = "Zakkenna-Korah!"
aisatsu me other    = "Domo, " ++ other ++ "-san. I'm " ++ me ++ "."

値を直接書く代わりに、パターンを書くこともできます。パターンは、リストやその他のデータ構造に対して使えます。例で見てみましょう。

例:人間不信に陥っただめぽ君(架空)は、標準ライブラリなんて信用できん!と言って、自前でmap関数(dMapと名付けることにする)を作ることにしました。

まず、だめぽ君はdMap関数の型を書きました。(これは必須ではありません)

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

次に、だめぽ君はリストが空の場合の定義を書きました。リストが空の場合は、dMap関数は空リストを返します。

dMap f [] = []

次は、リストが空じゃない場合です。空でないリストは、コロン演算子を使って x:xs の形に書けます(x がリストの先頭の要素で、xs が2番目以降の要素からなるリスト)。 x:xs の形をしたリストに対しては、等式 dMap f (x:xs) = (f x) : dMap f xs が成り立ちます。そこで、関数の引数の部分に (x:xs) と書いて、そのように定義します。

dMap f (x:xs) = (f x) : dMap f xs

これが、「リストが x:xs の形をしている場合」の dMap の定義です。このように、「リストが 〜〜 の形をしていた場合」という風な場合分けをすることを、パターンマッチと言います。この場合は x:xs がパターンです。

ここでは : 演算子を使ってパターンを書きました。: 演算子がパターンに使えるのは偶然ではなく、リストというデータ構造が「空リスト []、または要素 x とリスト xs に対して x:xs の形をしたもの」という風に定義されているからです。なので、例えば ++ 演算子はパターンに使えません。リストの末尾に要素を追加する演算子を自分で作ってやっても、それはパターンマッチには使えません。

ただ、リストの場合は例外として、要素を並べて書く [x,y,z] という書き方もパターンマッチに使えます。例を見てみましょう。

例:パラレルワールドのだめぽ君は、コロンを使ったパターンマッチを知らなかったので、リストの長さが長い時は標準ライブラリのmap関数に丸投げすることにしました。パラレルワールドのだめぽ君が書いたmap関数を見てみましょう。

dMap f [] = []
dMap f [x] = [f x]
dMap f [x,y] = [f x,f y]
dMap f [x,y,z] = [f x,f y,f z]
dMap f xs = map f xs

問:だめぽ君は、length関数の結果の型が Integer じゃないのが気に食わないと言い出し、length関数のようなものを自前で実装することにしました。

  1. 標準ライブラリの length 関数の型を調べてください。
  2. だめぽ君は dLength 関数を2行ほど書いたところで力尽きてしまいました。だめぽ君の代わりに dLength 関数を完成させてあげましょう。
dLength :: [a] -> Integer
dLength [] = 0

応用:関数の引数のところで、パターンと値を混在させることもできます。無駄に場合分けをして定義されたsum関数(mSum関数)を見てみましょう。

mSum :: [Integer] -> Integer
mSum [] = 0
mSum (x:[]) = x
mSum [0,x] = x
mSum [x,y] = x+y
mSum (0:xs) = mSum xs
mSum (x:x':xs) = x + x' + mSum xs

なお、引数が複数のパターンにマッチする場合は、最初にマッチした定義が使われます。

関数にまつわる演算子など

関数合成演算子 (.)

Haskellは関数型言語というだけあって、関数を組み合わせてプログラムを書くことが多いです。そこで、関数合成のための演算子が標準ライブラリに用意されています。

文法は、 f . g と書けば合成関数 f∘g になります。キーボードで丸を打てないのでピリオドで代用していますが、ほぼ数学の表記そのままですね。

例:文字列化する関数 show と、「1を加える」関数 (+ 1) を合成

Prelude> (show . (+ 1)) 3
"4"
Prelude> map (show . (+ 1)) [3,5,7]

["4","6","8"]

問:関数合成演算子 (.) の型は何でしょうか。まず自分で推測して、次にghciの :t コマンドで確かめてみましょう。

関数適用演算子 ($)

Haskellでの関数適用は、値を2つ並べるだけです(最初の値が関数です)。一方で、Haskellの標準ライブラリには関数適用演算子 ($) が用意されています。f $ x と書けば普通の関数適用 f x と等価になります。なんでわざわざこんな演算子が用意されているのでしょうか。

一つには、関数適用が入れ子になる時にカッコを削減したいというのがあります。例として、式 x + y に関数 f, g, h を順番に適用することを考えてみましょう。

h (g (f (x + y)))

カッコが目立ちますね。一方、関数適用演算子を使えばこう書けます。関数適用演算子は右結合(g $ f $ xg $ (f $ x) と解釈される)であることに注意してください。

h $ g $ f $ x + y

カッコがなくなりました。特に、右側の閉じカッコがなくなってすっきりしています。

もう一つは、リストなどに入った関数に一気に値を代入して関数適用するためです。関数からなるリスト [f, g, h, …] のそれぞれに x を代入して新しいリスト [f x, g x, h x, …] を作る操作が、関数適用演算子を使えば map ($ x) [f, g, h, …] と書けるのです。

例:

Prelude> map ($ 3) [(+ 1),(^ 3),(* 4)]
[4,27,12]

問:関数適用演算子の型は何でしょうか。予想して、確かめてみましょう。

恒等関数 id

Haskellの標準ライブラリには恒等関数 id が定義されています。便利ですね。

問:あなたはある日突然異世界に飛ばされました。その異世界にもHaskellという言語がありましたが、標準ライブラリに恒等関数は入っていないそうです。そこで、恒等関数 id をあなた自身の手で書いてみましょう。

定数関数 const

const関数を使うと、定数関数を作ることができます。const関数に値を渡して得られる const x という関数は、引数にかかわらず x という値を返します。つまり、

f :: a -> Integer
f = const 42

という関数を定義すれば、 f は引数にかかわらず 42 という値を返す定数関数になります。

例:

Prelude> const 42 "Don't ignore me!"
42
Prelude> map (const "sushi") ["ramen","tsukemen","curry and rice"]
["sushi","sushi","sushi"]


Comments