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
142つめの例では、ラムダ抽象に引数を 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 lcount1 の機能は単純だ。述語 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 + 54.
\x -> "hello, world"5.
\x -> x 'a'6.
\x -> x x7.
\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
などから入手できます。
日本語訳に関するご指摘は、コメントとしてお寄せください。

