この章に出てくるすべての例はHaskellのソースファイルに入力しGHCないしHugsに読み込ませることで評価することができる。入力の頭の"Prelude>"のプロンプトは含めないこと。プロンプトが出ているときは、GHCiといった環境にコードを入力してよい。そうでない時は、コードをファイルに入れて実行すること。 (斜体にするのは英字だけみたい。そしてEnter入ると切れるみたい。)
GHCiを電卓として使う方法はもう見た。もちろん、この方法は短い計算に取ってのみ実用的である。より長い計算やHaskellのプログラムを書くのには中間結果を追跡したい。
中間結果は変数に格納でき、名前で呼び出すことができる。1つの変数には1つの値が入っていて、変数が使われた時に変数名が値に代わる。例えば、以下に示すような計算である。
ghci> 3.1416 * 5^2 78.53999999999999
これは円の面積公式A = \pi r^2による半径5の円のおよその面積である。\pi (\approx 3.1416)の数桁打つのも、この数桁をきっかり覚えるのさえも面倒臭い。実際、プログラミングの重要な気の持ちようの1つは我々の頭が思う存分より面白い考えを扱うようにするために、非思考的な繰り返しや丸暗記を機械に任せることだ。今回の場合なら、Haskellはすでに10桁以上の\piを格納しているpiという変数が入っている。
ghci> pi 3.141592653589793 ghci> pi * 5^2 78.53981633974483
注意:変数piとそれが持っている値3.141592653589793は計算上では相互交換可能に使われうる。
(後ろに9が続くのは10進数で有限小数でも2進数では無限小数なために精度落ちするため。
ちなみに私は3.14159265358979323846264338327950288419716939937510まで覚えている)
一瞬だけ使うわけでないコードを書くときはいつでも、コードを拡張子が.hsのHaskellのソースファイルに保存する。基本的に、.hsファイルはプレーンテキストである。 テキストエディタにコーディングに合った予測機能が必要ならWikipediaのテキストエディタの記事から始めてみるのが良いだろう。VimやEmacsがHaskellプログラマの間でよく採用されている。(小生Emacsユーザーなのです、スミマセン)
しっかりとしたソースコードエディタには読解を楽にするために適切にでコードに色を付ける構文ハイライトが付いている。
きちんと管理するためにこの本での演習で作ることになるHaskellファイルを保存するためのディレクトリ(Windowsで言うところのフォルダ)を作っておくこと。このディレクトリをHaskellWikibookなりと呼ぶことにする。そしてそのディレクトリ内にVarfun.hsという名前のファイルを作り以下のコードを書く:
r = 5.0
このコードは変数rを5.0と定義している。自分で変数を設定すると後の計算をするのが容易になる。
注意:行頭にスペースが入っていないことを確かめること、というのもHaskellはスペースに厳格なのである。
次に、ターミナルをHaskellWikibooksディレクトリに持って行き、GHCiを起動し
:loadコマンドを使ってVarfun.hsファイルを読みこむ:
Prelude> :load Varfun.hs [1 of 1] Compiling Main ( Varfun.hs, interpreted ) Ok, modules loaded: Main.
:loadは:lと略せる。(今回なら:l Varfun.hsのように略せる)
もしGHCiがCould not find module 'Varfun.hs'('Varfun.hs'モジュールを見つけられませんでした)といったエラーを出した場合、違うディレクトリにいる可能性がある。
:cdコマンドでGHCiの中でディレクトリを変えることが出来る。(例えば、
:cd HaskellWikibook)
ファイルがロードされたのなら、新しく定義した変数rを計算に使うことが出来る。
*Main> r 5.0 *Main> pi * r^2 78.53981633974483
なので、半径5の円の面積を計算するために、単にr = 5.0と定義して、よく知られている円の面積公式\pi r^2を入力する。これにより毎回数を書く必要が無くなり、とても便利である。
この機能はとても便利だったので、更に定義を加えよう:ソースファイルの内容以下のように変更する。
r = 5.0
area = pi * r ^ 2
ファイルを保存し、新たな内容を読みこませるために:reload(省略形は:r)コマンドをGHCiに入力する。(このことはこのページの最後まで続く)
*Main> :reload Compiling Main ( Varfun.hs, interpreted ) Ok, modules loaded: Main. *Main>
今、rとareaの2つの変数が利用可能である。
*Main> area 78.53981633974483 *Main> area / r 15.707963267948966
|
注意 GHCiプロンプトからソースファイルを使わずに直接変数を定義することも可能である。詳しいことを省略すると、これをするための構文は Prelude> let area = pi * 5 ^ 2 便宜上このようにletを時々使うが、このやり方は一旦少しでも複雑な仕事に移ると不便である。そのため、最初からソースファイルを使うことを強調している。 (letは数学な世界で仮定の意味だと思われ、BASICにもだいたい省略可能でLETはある) |
|
注意 GHCはコンパイラとしても利用可能である。(つまり、自分の書いたHaskellファイルをインタプリタに頼らずに実行できるスタンドアロンプログラムに変換するためにGHCを使うことが出来る。)やり方についてはスタンドアロンプログラムの章で後々説明する。(Glasgow Haskell Compilerですから出来なきゃ詐欺) |
続きに行く前に、プログラム内に文章をコードとして扱われずに入れることが可能であることを理解するのが良い。これはコメントを使うことによって出来る。Haskellではコメントは-- で始め、行の最後まで続く:
x = 5 -- 変数xは5
y = 6 -- 変数yは6
-- z = 7
この場合、xとyは定義されているが、zは定義されていない。さらに、コメントは代わりの構文{-(コメント)-}を使うことでどこにでも入れることも出来る。(日本語も可能)
x = {-可能だからというだけの理由でやっている-} 5
一般にコメントは読者が混乱するかもしれないプログラムの部分を説明するのにその場所に使われる。 しかしながら使いすぎには注意すること。コメントが多すぎるとプログラムが読みにくくなる。 また、対応するコードを変更した時はコメントが古いのになったり、不正確であったり、間違いを引き起こしがちにならないようにいつもコメントを注意深く更新すること。
もしCのような命令型プログラミング言語に既に慣れているのなら、Haskellの変数があなたの知っている変数というものととても異なっていることに気付くことになる。では今からその理由と違いについて説明してゆく。
もしプログラミング経験がないのなら、この部分を読み飛ばして関数(うまく動作しせん)から読むのが良いかもしれない。
命令型言語の場合と異なり、Haskellでの変数は不変である。(←重要 斜体が出ないので)
つまり、一度定義すると、中の値は決して変化しない、イミュータブルなのである。例えば、以下のコードは動かない:
r = 5
r = 2
関数型プログラミング言語での変数は計算機のメモリの位置というよりも数学的な変数の方がより関連している。数学の授業では一つの問題の中で絶対に決して変数の中の値が変わることは無かっただろう。それ同様に、Haskellではコンパイラは上のコードに対し"multiple declarations of r"(rの多重宣言)とエラーを返すのだ。
計算機に何をするかを明示的に伝えることがある命令型プログラミングに慣れている人々にとってはこれを「最初r = 5と設定して、次にr = 2と変える」
と読むのに慣れているかもしれない。しかしながら、関数型プログラミングでプログラムは計算機のメモリで何をするのかを明らかにする責任がある。
これは命令型言語との大きな違いの別の例である:
r = r + 1
これは(命令型言語で考えがちな)"変数rのインクリメント"ではなく、
実際はそれ自身の観点では、rの再帰的定義である。(詳しくは再帰を後々説明する。命令型言語で起こるようなことと大きな違いがあることだけを覚えておけば良い)もしrが前に何かしらの値で定義されていたのなら、
r = r + 1はHaskellではエラーメッセージを出す。
これは数学的文脈で明白に間違いである5 = 5 + 1と言うのに同類である。
(逆に技術の教科書には、命令型プログラミングのa = a + 1に慣れろと書いてあった)
変数の値がプログラム中変わらないので、変数はいかなる順序でも定義できる。例えば、以下のコードの断片は全く同じことをする:
y = x * 2 |
x = 3 |
物事を好きな順番で書いても良い、つまり、「xがyの前に宣言された」かその逆かを気にする必要はない。また、このことは一回しかものを宣言できないことの理由でもある。もし2回以上宣言できたとすると、上のコードが曖昧になってしまう。もちろんのことだが、それでもyを使うにはxの値が必要であるが、これは具体的な数値が必要になるまで重要でない。
今までの説明で、もしかしたら変数が変化しないHaskellで実際一体何が出来るのかと思っているかもしれない。しかし、見捨てないでくれ、この本の残りの部分で説明しようと思っていることだが、変数を一つも変更すること無くこの世のどんなプログラムも書けるようになる。 実際変数が変わらないお陰でかなり楽になる、というのもそれのお陰でプログラムの挙動がずっと一層予測しやすくなるからだ。これは純粋関数型プログラミングの重要な特徴である。 そして、そのことで命令型プログラミングのやり方や考え方とかなり違ったそれらが必要なのである。
では、異なる半径の円の面積を計算したいとする。例えば、半径3の別の円の面積を計算してみよう。書いたばかりのプログラムから作るとすれば、rは既に5と定義されている。プログラム全体をr = 3と変更することが出来る。しかしすると、最初の円が計算できなくなる。代替策は新たな変数r2とr2で計算される面積のための変数area2を定義することだ。
この例が示すように、変数名には英字の他に数字含んで良い。(日本語はファイルからだとダメ、プロンプトからは可能)ただし、変数は小文字から始めなければならないが残りの部分については文字、数字、アンダースコア(_)、シングルクォート(')(と日本語がギリギリ)からなる任意の文字列を使うことが出来る(日本語には名前の1文字目の制約はない)。
2つの円を対象とした新しいソースファイルは以下のとおり:
r = 5
area = pi*r^2
r2 = 3
area2 = pi*r2^2
明らかなことだが、このやり方は円の面積公式を言葉通りに繰り返しているから不満足である。この頭使っていない繰り返しを無くすために一回だけ書いて異なる半径を適用させるのが好ましい。これがこそまさに関数のが出来ることである。
関数は引数値(パラメータともいう)を取り結果の値を与える(これは本質的に数学の関数と同じである)。Haskellでの関数の定義は単純である:変数を定義するように、ただし左辺に付ける関数の引数に気を払うこと。例えば、以下のコードはrと名前を付けた1引数に依る関数areaの定義である。
area r = pi * r^2
構文を注意深く見てみよう:関数名が最初に来て(上の例ではarea)
続いてスペースが一個そして引数(上の例ではr)とがある。
そして=の次に、関数の定義が既に定義した物々とともに筋通りに引数を使う公式がとしてある。
そして、関数の呼び出しごとに引数へ異なる値を入れることが出来る。コードをファイルに保存しGHCiに読み込ませて以下を試す:
*Main> area 5 78.53981633974483 *Main> area 3 28.274333882308138 *Main> area 17 907.9202768874502
ゆえに、この関数を異なる半径で呼び出して、いかなる半径の円の面積も計算できる。
ここでの関数は数学的に以下のように定義される
A(5) = 78.54やA(3) = 28.27のように数学では、パラメータは括弧によって囲まれる。しかし、Haskellのコードは括弧を付けても機能するが普通は省略される。Haskellは関数型言語なので、常に関数を使うことになる、なので可能である場合ならいつも余分な記号を最小にしたいのである。
また、括弧は式(値を与えるコード全てのこと)をともに評価させるためにまとめるのに使われる。以下の2つの式がいかに違う解釈をされるかに注意:
5 * 3 + 2 -- 15 + 2 = 17 (乗算は加算の前になされる)
5 * (3 + 2) -- 5 * 5 = 25 (括弧のおかげ)
area 5 * 3 -- (area 5) * 3
area (5 * 3) -- area 15
Haskellがどのように+や*と言った他のどの演算子よりも優先順序が先であるかに注意。これは例えば、数学で乗算が加算よりも先になされることと同じ流れである。
GHCiに式を入力したときに厳密に何が起こるのかを理解してみよう。Enterキーを押した後、
GHCiは渡された式を評価をする。これはそれぞれの関数を定義に置き換え、一つの値になるまで計算をするということだ。例えば、area 5の評価は以下のように進行する。
area 5
=> {左辺area rを右辺pi * r^2に変える}
pi * 5^2
=> {変数piを数値に変える}
3.141592653589793 * 5^2
=> {べき乗(^)を適用}
3.141592653589793 * 25
=> {乗算(*)を適用}
78.53981633974483
ここに示す通り、関数の適用や呼び出しというのは関数の定義の左辺を右辺に変えるということである。最後の工程として、GHCiは最終結果を画面に表示する。
関数を更に挙げてゆく:
double x = 2*x
quadruple x = double (double x)
square x = x*x
half x = x / 2
(quadrupleは定義を見れば分かる通り4倍のこと)
|
演習問題
|
もちろん、関数は2つ以上の引数を持つことも出来る。例えば、以下は幅と高さ(lengthとなっている)が与えられた長方形の面積を計算する関数である:
areaRect l w = l * w
*Main> areaRect 5 10 50
三角形の面積を計算する別の例:
\left(A = \frac{bh}{2}\right)(底辺×高さ÷2で求める)
areaTriangle b h = (b * h) / 2
*Main> areaTriangle 3 9 13.5
例から分かる通り、複数の引数はスペース(全角半角は問わない)で分ける。これも
式をまとめるのに括弧を使わなければならないことがある理由である。例えば、値
xを4倍するのに以下のようには書けない
quadruple x = double double x
理由はこの書き方はdoubleという名前の関数を2つの引数doubleとxに適用する意図となるからである、というのも関数は他の関数の引数になりうる(後々理由を説明する)。代わりの、引数の周りに括弧を付けなければならない:
<blckquote> quadruple x = double (double x) </blockquote>
引数は常に与えられた順番通りに渡される。例えば:
subtract x y = x - y
*Main> subtract 10 5 5 *Main> subtract 5 10 -5
ここの例ではsubtract 10 5は10 - 5に評価するが、
subtract 5 10は順番が違うので5 - 10に評価する。
|
演習問題
ヒント:ピラミッドの体積と1ブロックの体積を見積もる必要がある。(Wikipediaに大きさが載っているが答えの石の数も書いてあるのが難点。また中に穴が開いているということ気にしなくて良い) |
言わずもがな、加算(+)や乗算(*)といった予め定義された関数がが使えるのと同様に、新しい関数を定義するのに既に定義した関数を使うことができる。(Haskellでは演算子は関数として定義されている)例えば、正方形の面積を計算するために、
長方形の面積を計算する関数を再利用することが出来る:
areaRect l w = l * w
areaSquare s = areaRect s s
*Main> areaSquare 5 25
そもそも、正方形は辺の長さが等しい長方形なだけである。
この原則はかなり単純に思えるかもしれないが、とても強力である。特に、数字の代わりに他のオブジェクトで計算し始めるときはそうである。
|
演習問題
|
where節関数を定義する際、関数にローカルな中間の結果を定義することはよくある。例えば、
3辺の長さa、b、cが与えられている三角形の面積を計算するヘロンの公式A = \sqrt{s(s-a)(s-b)(s-c)}を考える:
heron a b c = sqrt (s*(s-a)*(s-b)*(s-c))
□・・where
□・・s = (a+b+c) / 2
変数sは三角形の周の長さの半分で平方根を求める関数sqrt
の引数の中に4回書き出すのはただ長いだけで退屈である。
実は単に定義を順に書き連ねるだけではうまくいかない――
heron a b c = sqrt (s*(s-a)*(s-b)*(s-c)) -- sはここでは定義されていない
s = (a+b+c) / 2 -- a,b,cはここで定義されていない
――なぜなら変数a、b、cは関数heronの右辺の中のみで利用可能であるが、ここに書かれているsの定義はheronの右辺の外であるからだ。この式を右辺の一部にするために、
whereキーワードを使う必要がある。
下に続く定義と区別するためにwhereとローカル定義がスペース4つでインデントされていることに注意。(スペースがうまく出力されないので"□・・"と書いた。
入力の際にはスペースに変えること)以下はローカルとトップレベルの定義を併用した例である。(fig.1)
areaTriangleTrig a b c = c * height / 2 --三角法を用いた方法
□・・where
□・・cosa = (b^2 + c^2 - a^2) / (2*b*c) --余弦定理
□・・sina = sqrt (1 - cosa^2) --cos x ^2+sin x ^ 2 =1
□・・height = b*sina
areaTriangleHeron a b c = result --ヘロンの公式を用いた方法
□・・where
□・・result = sqrt (s*(s-a)*(s-b)*(s-c))
□・・s = (a+b+c)/2
さっきの例を注意深く見ると、変数名a、b、cを
2つの面積を計算する関数のそれぞれ1回ずつ、計2回ずつ使っていることに気づくだろう。
これが正しく動作する仕組みについて説明してゆく。
幸運にも、以下のコードの断片はは嬉しくない驚きは無い:
Prelude> let r = 0 Prelude> let area r = pi * r ^ 2 Prelude> area 5 78.53981633974483
ここでの"嬉しくない驚き"とはlet r = 0の定義が入り込んで、areaの結果
0を返すことである。これはrを2回目に定義したとき、1回目の
rと違うrについて話題にしているからだ。これは実生活でも
起こることだ。あなたは、Johnという名前の人を何人知っているか?Johnという名前の人
に関して興味のあることは、殆どの場合、友達と"John"について話ができることであり、
文脈により、どのJohnを指しているのかが友達がわかることで、このことをスコープと呼ぶ。
スコープの専門的な裏側については説明しないつもりである(少なくとも今回は)。なので、 パラメータの値は関数の定義でどの変数を呼び出したかによらず厳格に関数を呼び出す時に渡した通りであるということを覚えておくだけでよい。
|
補足
上の例では Prelude>r を試すことで間違いであると判る。(0と帰ってくる)よって、この2つの |
そしてコメントはソースファイルの中のコードでない文章であることも学んだ