Hakell では、関数はれっきとしたクラス値である。つまり、
1
や 'c'
が型を持つ値であるのとまったく同じである。これは、square
や ++
といった関数についても言える。関数についてあれこれ述べる前に、少し横道に逸れてごく理論的なコンピュータサイエンスの分野に分け入って (それほど苦痛なものにはならないと思うのでご心配なく)、ラムダ計算について述べておく必要がある。“ラムダ計算”という名称は、もしかするととっつきにくい感じを受けるかもしれないが、関数を表現するきわめて単純なシステムを表している。ラムダ計算風に平方関数を書くと、λx.x*x のようになる。これは、値を1つ取り、それを x と呼び (それが“λx.”の意味である)、x 同士を掛け合わせる、という意味だ。λは“ラムダ抽象”と呼ばれる。λはふつう、パラメータを1つだけ取ることができる。2つの数をとる関数を書きたいなら、はじめの部分をもう1つ用意して2番目に加え、λxλy.2*x+y のように書く。ラムダ式に値を適用するときは、一番外側のλを取り除き、ラムダ変数をすべて値に置き換える。たとえば (λx.x * x)5 を計算する場合、λを取り除き、x をすべて 5 に置き換え、(5 * 5) つまり 25 となる。
実際、Hakell はラムダ計算の拡張を元にしているところが多く、この2つの式は Hakell で直接記述できる (λをバックスラッシュに置き換え、. を矢印に置き換える。また、λは繰り返し書く必要は無い。そしてもちろん、Haskell では、関数を定義すればそれに名前をつけることができる)。
square = \x -> x*x
f = \x y -> 2*x + y
ラムダ式を対話シェルで計算することもできる。
Prelude> (\x -> x*x) 5
25
Prelude> (\x y -> 2*x + y) 5 4
14
2つめの例では、ラムダ抽象に引数を 2 つ (1つは x、もう1つは y) 与える必要があることがわかる。
関数には“高階型”という名称が与えられている。関数に与えられる型は、関数のラムダ計算の表現によく似ている。たとえば、平方は、λx.x * x と定義される。この型は、まず
x
の型が何であるかを考えてみるとわかる。たとえば x
が Int であったとしよう。すると、関数 square
は Int を引数に取り、値 x*x
を生成することがわかる。2つの整数値を掛け合わせると Int になることがわかっているので、squre
の結果の型も Int である。ゆえに、squre
の型は、Int → Int であると言う。前述の関数
f
についても、同じように考察できる。この関数の値 (関数は値であることを思い出してほしい) は値 x
を引数に取り、その値から新しい値を生成する。その値は値 y
を引数に取り、2*x+y
を生成する。たとえば f
の場合、数値を 1 つだけ適用したとすると、(λxλy.2x + y)5 となり、新しい値 λy.2(5) + y となる。ここでは、x すべてを適用した値 5 で置き換えている。だから、f は Int を引数に取り、何らかの型 (それが何だかはっきりしないが) の値を生成することは知っている。しかし、この値の型は λy.2(5) + y の型であることはわかっている。上記の考察を当てはめると、この式は Int → Int という型をもつことがわかる。ゆえに、
f
は Int を引数に取り、Int → Int という型を持つ何かを生成する。だから、f
の型は Int → (Int → Int) である。■注■ 括弧は必須ではない。関数型では、α → β → γ であれば β → γ がグループ化されているとみなされる。もし、α → β がグループ化されているとしたい場合は、そこを括弧で囲む必要がある。
これは完全に正確なわけではない。以前述べたとおり、
5
のような数値は、実際には Int 型ではなく、Num a ⇒ a 型である。Prelude 関数の型は、前述のとおり、":t" を使えば容易にわかる。
Prelude> :t head
head :: [a] -> a
Prelude> :t tail
tail :: [a] -> [a]
Prelude> :t null
null :: [a] -> Bool
Prelude> :t fst
fst :: (a,b) -> a
Prelude> :t snd
snd :: (a,b) -> b
これは次のような意味である。"head" は型 "a" の値を含むリストを 1 つ引数に取り、型 "a" の値を返す関数。"tail" は "a" のリストを 1 つ引数に取り、"a" のリストを返す。"null" は "a" のリストを 1 つ引数に取り、真偽値を返す。"fst" は型 "(a,b)" のペアを 1 つ引数に取り、型 "a" の何かを返す。などである。
■注■
fst
の型が (a, b) → a であっても、必ずしも単純に最初の要素を返すことを意味するわけではない。返す値が最初の要素と型が同じであることを意味するだけである。+
、*
、++
、:
のような演算子の型を得ることもできる。ただし、括弧でくくる必要がある。ふつう、二項演算関数として使う関数 (つまり、2 つの引数の前ではなく、間に置く関数) はすべて、型を調べるときには括弧でくくる必要がある。Prelude> :t (+)
(+) :: Num a => a -> a -> a
Prelude> :t (*)
(*) :: Num a => a -> a -> a
Prelude> :t (++)
(++) :: [a] -> [a] -> [a]
Prelude> :t (:)
(:) :: a -> [a] -> [a]
+
と *
の型は同じで、+
は Num のインスタンスのある型 a に対し、型 a の値を 1 つ引数に取り、型 a の値を 1 つ引数に取って型 a の値を生成する別の関数を生成する。簡潔に言うなら、+
は型 a の値を 2 つ引数に取り、型 a の値を生成するとも言えるが、やや正確さに欠ける。++
の型の意味は、簡潔に言えば、与えられた a に対し、++
が a のリストを2つ引数に取り、新たな a のリストを生成する、ということだ。同様に、:
は型 a の値1つと型 [a] の別の値 (つまり a のリスト) 1つを引数に取り、型 [a] の別の値を生成する。putStrLn
といった関数の型を調べようと思うかもしれない。Prelude> :t putStrLn
putStrLn :: String -> IO ()
Prelude> :t readFile
readFile :: FilePath -> IO String
いったい、この IO というのは何だろうか? 簡単にいってしまえば、関数が本当の関数ではない時、Haskell はこういう表現をするのだ。これを、"IO アクション" (以後、
IO
) と言う。すぐにこんな疑問が生じるだろう。それはそうと、IO を出させないようにするにはどうすればよいのだろうか? 手短に言うと、直接取り除くことはできない。つまり、IO String → String 型の関数を書くことはできない。IO 型の物を使うには、たとえば do 記述を使って別の関数と結びつけるしかない。たとえば
readFile
を使ってファイルを読み込んでいるとき、おそらく、返される文字列に対して何かをしたいと思うだろう (そうでなければ、そもそもなぜファイルを読み込んだのだろう?)。String を引数に取り Int を生成する関数 f
があるとしよう。f
の入力は String だが、readFile
の出力は IOString で一致しないため、readFile
の結果を直接 f
に入れることはできない。しかし、以下のようにすると結びつけることができる。main = do
s <- readFile "somefile"
let i = f s
putStrLn (show i)
ここで、矢印記号を使って "IOアクションから文字列を取り出し"、その文字列 (
s
としている) に f
を適用している。そして、たとえば i
を画面上に出力する。ここで使われている let には対応する in が無いことに注意してほしい。これは、do ブロックの中だからである。また、f
はIOアクションではなく単なる通常の関数なので、i <- f s
とは書かないことにも注意が必要である。以下のような理由で、ある要素や関数の型を明示的に指定したい時がある。
- 明瞭性
- 速度
- デバッグ
型定義は関数定義とは別に記述する。たとえば、以下のようなコードで
square
関数の型を指定できる (明示的に定義した型を 型シグネチャ と言う)。square :: Num a => a -> a
square x = x*x
この2行は、続けて記述する必要すらない。ただし、指定する型は、関数定義から推測される型と一致しなければならない。この定義では、
square
は Num のインスタンス (Int、Double、など) の平方が適用できる。ただし、square
がInt型の値にだけ適用されるという前提条件を知っていれば、その型を以下のように精錬することができる。square :: Int -> Int
square x = x*x
これで、squre は Int 型の値にのみ適用できる。さらにこの定義では、
squre
には Int 型の値しか適用しないことがわかっているので、コンパイラは変更前の定義で指定していたような一般的なコードを生成する必要が無く、実行速度の速いコードを生成することができるだろう。もし、拡張機能を有効にしてあるならば (Hugs では "-98"、GHC(i)では "-fglasgow-exts")、関数だけでなく、拡張機能にも型シグネチャを追加できる。たとえば、以下のように書くことができる。
square (x :: Int) = x*x
この定義は、
x
が Int であることをコンパイラに教えるが、式の残りの部分の型はコンパイラの推論に任されている。この例の squre
の型は何だろうか? まずは考えてみよう。答えはこのコードをファイルに入力してインタープリタにロードするか、式の型を尋ねてみれば分かる。Prelude> :t (\(x :: Int) -> x*x)
このラムダ抽象は上記の関数定義と同等である。
第3.3節で、関数を引数に取る関数の例を考察した。たとえば、
map
は、関数を1つ引数に取り、その関数をリストの各要素に適用する。filter
は、リストのどの要素を残すかを判定する関数を1つ引数に取る。foldl
は、リストの要素の結合方法を与える関数を1つ引数に取る。Haskell のほかの関数と同様、これにもきちんとした型がある。まず、
map
関数について考えてみよう。map の役割は要素のリストを引数に取り、別の要素のリストを生成することだ。この2つのリストは、必ずしも同じ型の要素を持つ必要はない。だから、map
は、型 [a] の値を引数に取り、型 [b] の値を生成する。map
関数は、それをどのように行っているのだろうか? ユーザーが提供する関数を用いて変換を行っている。1 つの a を 1 つの b に変換するためには、この関数の型は a → b でなければならない。ゆえに、map
の型は(a → b) → [a] → [b] である。これはインタープリタの ":t" で確かめられる。同様の考察は
filter
にも当てはまり、(a → Bool) → [a] → [a] という型を持つことがわかる。foldl
関数については、(a → a → a) → a → [a] → a という型だと考えたくなるかもしれない。実際 foldl
は、より一般的な型 (a → b → a) → a → [b] → a を持っている。だから、1 つの a と 1 つの b を 1 つの a に変える関数 1 つと、型 a の 初期値 1 つと、型 b のリスト 1 つを引数に取る。これは型 a を生成する。これを確かめるには、与えられた制約を満たすリストのメンバーがいくつあるかを数える関数
count
を書けばよい。もちろん filter
と length
を使ってもよいが、foldr
を使うこともできる。module Count
where
import Char
count1 p l = length (filter p l)
count2 p l = foldr (\x c -> if p x then c+1 else c) 0 l
count1
の機能は単純だ。述語 p
に従ってリスト l
にフィルターをかけ、その結果得られたリストの長さを引数に取る。一方、count2
は初期値 (これは整数値である) を使って現在のカウント値を保持する。count2
は、リスト l
の各要素に対して、上記のラムダ式を適用する。このラムダ式は 2 つの引数を取る。現在のカウント値 c
と、リスト中の対象となる要素 x
である。ラムダ式は、x
が p
を満たすかどうかを調べる。もし満たすなら、述語を満たす要素の数を 1 つ増やして c+1
を返す。満たさないなら、そのまま c
を返す。演習
演習4.3 以下の式を自分で計算せよ。また、もしそれが型を持つなら、その型を確かめよ。式が型エラーを起こすかどうかにも注意すること。
1.
2.
3.
4.
5.
6.
7.
\x -> [x]
2.
\x y z -> (x,y:z:[])
3.
\x -> x + 5
4.
\x -> "hello, world"
5.
\x -> x 'a'
6.
\x -> x x
7.
\x -> x + x
前ページ「4.3 型クラス」
次ページ「4.5 データ型」
1 はじめに
2 スタートガイド
4 型の基礎
5 Basic Input/Output
6 Modules
7 Advanced Features
8 Advanced Types
9 Monads
10 Advanced Techniques
A Brief Complexity Theory
B Recursion and Induction
C Solutions To Exercises
これは、Haskell (ハスケル) のチュートリアル "Yet Another Haskell Tutorial" を日本語に翻訳したものです。
オリジナルのドキュメントは、
http://www.cs.utah.edu/~hal/docs/daume02yaht.pdf
などから入手できます。
日本語訳に関するご指摘は、コメントとしてお寄せください。
オリジナルのドキュメントは、
http://www.cs.utah.edu/~hal/docs/daume02yaht.pdf
などから入手できます。
日本語訳に関するご指摘は、コメントとしてお寄せください。