人気ブログランキングへ

2009年03月10日

Yet Another Haskell Tutorial (和訳): 3.5 関数

3.5 関数

コードをファイルに書く方法を学んだので、関数を書き始めることができる。関数型言語であることから予想がつくとおり、関数がHaskellの中心である。つまり、プログラムの評価は関数の評価に過ぎない。
数値の2乗を計算する簡単な関数を書き、ファイルTest.hsに入力することができる。この関数は以下のように定義できる。

square x = x * x

この関数定義では、引数 (パラメータともいう) を1つとるsquareという関数を定義しており、引数名をxとしている。そして、square xの値がx * xに等しいとしている。
Haskellは標準的な条件式もサポートしている。たとえば、引数が0未満の場合は-1を、引数が0の場合は0を、引数が0より大きい場合は1を返す関数を定義できる (これを符号関数 (signum function) という)。

signum x =
if x < 0
then -1
else if x > 0
then 1
else 0

この関数を試すと、以下のようになる。

Test> signum 5
1
Test> signum 0
0
Test> signum (5-10)
-1
Test> signum (-1)
-1

最後の例で、"-1" には括弧をつける必要がある。括弧をつけないとsignumという値から1を引こうとしているとシステムに解釈されてしまい、型が正しくない。
Haskellのif/then/else構造は、他のほとんどの言語ととてもよく似ている。しかし、then節とelse節の両方が必須である。条件式は条件を評価する (この場合は、x < 0。もしこれがTrueと評価されれば条件thenを評価する。Falseと評価されれば条件elseを評価する)。
ファイルを編集してインタープリタにロードし直せば、このプログラムをテストできる。作業中のモジュールがすでにTestになっている場合は、再度:l Test.hsと入力する代わりに、:reloadまたは:rと入力するだけでリロードされる。通常は、このほうがずっと早い。
Haskellも、他の多くの言語と同じようにcase構文をサポートしている。case構文は、確かめたい値が複数存在する場合に使われる (case式は、実際にはそれよりももう少し強力な機能を持つ。詳細は7.4節を参照)。
引数が0なら1、1なら5、2なら2、それ以外なら-1となる関数を定義したいとしよう。この関数をif文を使って書くと長くてかなり読みにくいものとなるだろう。そこで、以下のようにcase文を使って書く (この関数をfとする)。

f x =
case x of
0 -> 1
1 -> 5
2 -> 2
_ -> -1

このプログラムでは、引数xをとり、xの値を調べる関数fを定義している。もしxの値が0に等しければ、fの値は1になる。xの値が1に等しければ、fの値は5になる。xの値が2に等しければ、fの値は2になる。そして、その時点でどれにも等しくなければ、fの値は-1になる (アンダースコアは、すべてにマッチする「ワイルドカード」とみなされる)。
ここでは、字下げが重要である。Haskellでは、コードを構造化するために「レイアウト」という仕組みが使われている (プログラミング言語Pythonが同様の仕組みを使用している)。CやJavaではセミコロンや中括弧が必須だが、レイアウトシステムを使えばセミコロンや括弧を明示的に使うことなくコードを書くことができる。

■警告■ Haskellは空白文字が重要なので、タブを使っているのかスペースを使っているのか注意する必要がある。エディタでタブを使わないように設定できるなら、おそらくそのように設定しておいたほうがよい。もしなければ、タブを8桁にしておけば問題が起きにくくなる。

レイアウトは一般的に次のように解釈される。キーワードwhereletdoofの後に左中括弧が挿入され、次のコマンド行のインデント位置が記憶される。次の行以降、インデント位置が同じであれば直前の行末にセミコロンが挿入される。次の行のインデント幅が狭ければ、右中括弧が挿入される。複雑に感じるかもしれないが、これらのキーワードの後のインデントに関する原則に従えば覚える必要はなくなる (レイアウトに関する詳細な説明は7.11節を参照)。
レイアウトを使うことを好まず、中括弧とセミコロンを明示的に使う人もいる。そういった書き方もまったく問題なくできる。その書式の場合、上の関数は以下のようになる。

f x = case x of
{ 0 -> 1 ; 1 -> 5 ; 2 -> 2 ; _ -> 1 }

もちろん、中括弧とセミコロンを明示的に使えば、コードを自由な構造で書くことができる。以下のコードも同じく有効である。

f x =
case x of { 0 -> 1 ;
1 -> 5 ; 2 -> 2
; _ -> 1 }

しかし、コードの構造をこのようにすると、コードが読めなくなるだけだ (この例のように)。
関数は部分的に定義していくこともできる。つまり、あるパラメータに対する関数の内容を書き、そして別のパラメータに対する別の内容を書くのだ。たとえば、上の関数fは、以下のように書くこともできる。

f 0 = 1
f 1 = 5
f 2 = 2
f _ = -1

このとき、順序が重要となる。最後の行を最初に書くとすべての引数にマッチすることになり、fは引数によらず-1を返すようになる (ただし、ほとんどのコンパイラは、パターンが重なり合っているなどといった警告を表示する)。この最後の行を指定しないと、0、1、2以外が指定されるとfはエラーとなる (ほとんどのコンパイラは、この場合もパターンが完全ではないなどといった警告を表示する)。このように部分的に定義していく方法はよく使われ、このチュートリアルでもかなり頻繁に使用する。fのこの2種類の定義は実際に同等で、部分的に定義していく方法はcase式に変換される。
単純な関数から、関数合成 (function composition) を使ってより複雑な関数を構築できる。関数合成は単純に、ある関数の適用結果を別の関数の引数として使う方法である。この方法については、すでに計算に関する節 (3.1節) で、5*4+3について書いたところで述べている。その例では、5 * 4を評価し、その結果に+ 3を適用している。関数squrefでも同じことができる。

Test> square (f 1)
25
Test> square (f 2)
4
Test> f (square 1)
5
Test> f (square 2)
-1

上記の関数の適用結果はじつに明確だ。内側の関数を囲む括弧は必須である。括弧をつけないと、たとえば最初の行ではインタープリタは "square f" の値を得ようとしていると解釈し、意味をなさなくなる。このような関数の適用方法は、ほとんどのプログラミング言語でごく一般的である。もう1つ、より数学的指向性の高い関数合成の表現方法がある。それは、関数 (.) (ピリオド1つ) を使う方法だ。この関数 (.) は、数学の演算子 (o) に見立てたものだろう。

■注■数学ではf o gと書くと「gの適用後にfを適用」を意味し、Haskellでもf . gと書くと「gの適用後にfを適用」を意味する。
f o gは単純に、(f o g)(x) = f(g(x))を意味する。つまり、値xに関数f o gを適用することは、gを適用した後にfを適用することに等しい。

関数 (.) (関数合成関数という) は2つの関数を引数にとり、それを1つの関数にする。たとえば (square . f) と書くと、1つの引数をとり、その引数にfを適用した後、その結果にsquareを適用する新しい関数が生成されることを意味する。逆に (f . square) は、1つの引数をとり、その引数にsquareを適用した後、その結果にfを適用する新しい関数が生成されることを意味する。これまでと同様、テストすればこのことがわかる。

Test> (square . f) 1
25
Test> (square . f) 2
4
Test> (f . square) 1
5
Test> (f . square) 2
-1

ここで、関数合成は括弧で囲まなければならない。そうしないとHaskellコンパイラは、最初の例では f 1 の値にsquareを合成しようとしていると解釈するが、f 1 は関数でさえないので意味をなさない。
おそらく、ここでいったん話を中断し、Preludeで定義されている関数をいくつか見てみるのが賢明だろう。まず間違いなく、既存の関数を書き換えてしまうミスをいずれ起こすものだ (私は数え切れないほどそういう誤りを犯した)。しかし、それを最低限に抑えられればかなり時間を節約できる。以下、簡単な関数をいくつか挙げる。そのうちのいくつかは、すでに触れたものだ。
sqrt平方根関数
id恒等関数。id x = x
fstペアから最初の要素を取り出す
sndペアから2番目の要素を取り出す
nullリストが空かどうかを返す
head空でないリストの最初の要素を返す
tail空でないリストの最初の要素以外を返す
++2つのリストを結合させる
==2つの要素が等しいかどうか調べる
/=1つの要素が等しくないかどうか調べる

以下、各関数の使用例を示す。

Prelude> sqrt 2
1.41421
Prelude> id "hello"
"hello"
Prelude> id 5
5
Prelude> fst (5,2)
5
Prelude> snd (5,2)
2
Prelude> null []
True
Prelude> null [1,2,3,4]
False
Prelude> head [1,2,3,4]
1
Prelude> tail [1,2,3,4]
[2,3,4]
Prelude> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]
Prelude> [1,2,3] == [1,2,3]
True
Prelude> 'a' /= 'b'
True
Prelude> head []
Program error: {head []}

空のリストにheadを適用するとエラーになることがわかる (正確なエラーメッセージはGHCiを使っているかHugsを使っているかにより異なる。上の例はHugsのエラーメッセージである)。

3.5.1 束縛let

関数内でローカル定義を利用したくなることがよくある。たとえば、学校で習った数学を思い出してみよう。以下の等式は ax2 + bx + c = 0 の形の多項式の根 (零点) を求めるのに使われる。x = (−b ± √b2 − 4ac)/2a この2種類の x の値を計算する、以下のような関数が記述できるだろう。

roots a b c =
((-b + sqrt(b*b - 4*a*c)) / (2*a),
(-b - sqrt(b*b - 4*a*c)) / (2*a))

この問題を改善するために、Haskellではローカル束縛が可能となっている。ローカル束縛とは、その関数だけが参照できる値を関数内に作れるというものだ。たとえば、sqrt(b*b-4*a*c) のローカル束縛を作成し、それにdet などの名前を付け、sqrt(b*b-4*a*c) が現れる箇所を両方ともそれに置き換えてみよう。これは、let/in 定義を使って実現できる。

roots a b c =
let det = sqrt (b*b - 4*a*c)
in ((-b + det) / (2*a),
(-b - det) / (2*a))

じつは、let内には複数の定義を書くこともできる。なお、レイアウトの問題が生じないように必ずインデント幅を同じにしておくこと。

roots a b c =
let det = sqrt (b*b - 4*a*c)
twice_a = 2*a
in ((-b + det) / twice_a,
(-b - det) / twice_a)


3.5.2 2項演算関数

2項演算関数は、文字というより記号から成る関数である。たとえば、(+)(*)(++)はすべて2項演算関数である。2項演算関数は、括弧で囲めば前置形式で使える。したがって、以下の2つの式は同じである。

Prelude> 5 + 10
15
Prelude> (+) 5 10
15

同様に、(mapのような) 前置関数はバッククォート (英語キーボードでは、チルダキーをクリックすると表示される) で囲めば2項演算関数にすることができる。

Prelude> map Char.toUpper "Hello World"
"HELLO WORLD"
Prelude> Char.toUpper `map` "Hello World"
"HELLO WORLD"


■警告■ Hugsユーザーの場合: HugsではChar.toUpperのような修飾付きの名前は好まれない。Hugsでは、単にtoUpperを使う。


前ページ「3.4 ソースコードファイル
次ページ「3.6 コメント


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
などから入手できます。
日本語訳に関するご指摘は、コメントとしてお寄せください。

posted by K/I at 21:02 | 東京 ☁ | Comment(0) | TrackBack(0) | Yet Another Haskell Tutorial | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

※ブログオーナーが承認したコメントのみ表示されます。

この記事へのトラックバック
×

この広告は90日以上新しい記事の投稿がないブログに表示されております。