HOME Haskell のお勉強 書き込む

3. 関数を定義する


この文章では Haskell で関数を定義する方法について述べます。

1. = は関数定義

Haskell には代入が無いので、= を関数定義に使います。
foo x y = x + y
hoge    = 2
foo は2つの引数を取って、それらの和を返す関数です。一方、 hoge は引数を取らず、常に 2 を返す定数関数です。 これらをファイル(例えば a.hs)に書いたのちロードすると次のようになります。
Prelude> :l a.hs
Main> foo 1 2
3
Main> hoge
2
ちなみにプロンプトには hugs が最後に読み込んだモジュール名が表示されます。 hugs が立ち上がるとき Prelude というモジュールが読み込まれるので表示が Prelude になっています。一方、a.hs を読み込むと Main になるのは、 モジュール名を省略すると Main とみなされるからです。

2. 分岐

分岐には次の4通りがあります。
  1. 引数のパターンマッチング
  2. guard
  3. if-then-else
  4. case-of
これらの書式を用いて階乗を返す関数がどう書けるかを次に示します。
01:     -- pattern matching
02:     factA 0 = 1
03:     factA n = n * factA (n-1)
04:     
05:     -- guard
06:     factB n | n==0      = 1
07:             | otherwise = n * factB (n-1)
08:     
09:     -- if
10:     factC n = if n==0
11:                   then 1
12:                   else n * factC (n-1)
13:     
14:     -- case of
15:     factD n = case n of
16:                  0 -> 1
17:                  _ -> n * factD (n-1)

2.1. パターンマッチング

上のコードの 1--3 行目に示した通り、ある特定の引数に対する値を分けて書くことが出来ます。 この場合、特定性の高い順番に書きます。 2 行目と 3 行目を入れ替えると、factA は 0 で止らなくなります。 また、関数定義で参照されない任意の引数は _ であらわします。
例:
bar _ = "I don't care arguments."
Main> bar "Do you mind arguments?"
"I don't care arguments."

2.2. guard

条件によって処理を変えます。書式は以下の通りです。
function_name   arguments | predicate_1 = body_1
                          | predicate_2 = body_2
                           ....................
                          | otherwise   = body_otherwise
例:
01:     greeting is_woman age | is_woman && age < 20  = "I have never seen such a beautiful girl like you!"
02:                           | is_woman              = "Nice to meet you."
03:                           | otherwise             = "Sorry, I am busy now."
Main> greeting True 17
"I have never seen such a beautiful girl like you!"
Main> greeting True 28
"Nice to meet you."
Main> greeting False 21
"Sorry, I am busy now."
guard も特定性の高いものから書きます。 上の例で1行目の guard と2行目を入れ替えると age はどうでもよくなります。 また、otherwise は残り全てにマッチします。

2.3. if

Haskell にもおなじみの if--then--else があります。 Haskell の if は必ず else 節が必要です。
check n = if n > 0
              then "OK"
              else "Error"

2.4. case -- of

値によって処理を振り分けます。
score c = case c of
            'a' -> 90
            'b' -> 70
            'c' -> 50
            'd' -> 30
            _   ->  0
_ は列記されている値以外の全てにマッチします。

3. 補助関数

ある関数内部の補助関数を定義することが出来ます。Lisp の labels のようなものです。 補助関数の定義の方法は let を使う場合と where を使う場合の2通りありますが、 どちらかというと where を使うほうが一般的でしょう。

例として、砲丸投げで、砲丸の飛ぶ距離を求める関数を書いてみました。 飛ぶ距離は初速度(m s-1) と地面との角度(度)の関数です。

3.1. where を使った補助関数定義

where を使うと次のようになります。ほとんど高校の物理の教科書に書いてある通りなので 説明は不要でしょう。
shot_putting1 v0 angle = vx * t
 where r   = angle * pi / 180.0
       vx  = v0 * cos r
       vy  = v0 * sin r
       t   = 2 * vy / 9.8
whereguard が併用されている場合、全ての guardwhere の定義を参照できます。

3.2. let を使った補助関数定義

同じことを let を使って書くと次のようになります。
shot_putting2 v0 angle = let r   = angle * pi / 180.0
                             vx  = v0 * cos r
                             vy  = v0 * sin r
                             t   = 2 * vy / 9.8
                         in vx * t

4. 再帰

Haskell は scheme と同様に、反復のための構文はありません。 反復したいときは再帰で表します。
例:商と余りを求める
dev_mod i j = iter i 0
 where iter mod dev | mod < j     = (dev, mod)
                    | otherwise = iter (mod - j) (dev + 1)
上の例では、単純に割られる数から割る数を引き、何回引けたかを数えます。 最後に引けなくなったときの、引けた回数が商で、残りが余りです。 引き算の回数を記録するために、記録用の引数を持った内部関数 iter を使っています。 iter の内部では、割る数 j が定義されていないので、 外側のクロージャーの j が使われます。

5. 無名関数

無名関数は以下の様に \-> を使って定義します。 \ の 後に仮引数を並べて書き、 -> の後に本体を書きます。
\argvs -> body

-- for example 
\ x y -> x + y

6. その他

6.1. 関数と演算子との関係

+, -, *, /, ==, <, > などの演算子は2つの引数をとる関数 (+), (-), (*), (/), (==), (<), (>) を中置記法で表したものです。 任意の2引数の関数は ` で囲むことにより 中置記法に変換できます。
x + y ⇔ (+) x y
max a b ⇔ a `max` b

6.2. 構文糖衣

$
$$ がある位置から式の最後までをくくります。
foo $ hoge x y ⇔ foo (hoge x y)

6.3. 関数の合成

. は関数を合成します。
foo $ hoge x ⇔ (foo . hoge) x 

7. インデント

7.1. 明示的なブロック構造

Haskell は C と同じように、ブロック構造を明示することが出来ます。 ブロックの開始と終了はそれぞれ {} で、 式の区切りは ; で表します。 前述の shot_putting2 を明示的なブロック構造で書くと以下のようになります。
shot_putting3 v0 angle  = let{ r   = angle * pi / 180.0;
                               vx  = v0 * cos r;
                               vy  = v0 * sin r;
                               t   = 2 * vy / 9.8;
                              } in vx * t
明示的に表記すると処理系はインデントを無視します。 また、明示的な表記と、インデントによる表記を共存させることができます。

7.2. インデントによるブロック構造(レイアウト)

Haskell は Python と同様にインデントでブロック構造をあらわすことが出来ます。 インデント表記から明示的表記への変換規則は以下の通りです。
  1. where, let, do, of の後にブロックの開始を示す { を挿入する。
  2. その後、同様にインデントされた行はブロック内の式とみなし、 先頭に ; を挿入する。
  3. インデント量が減少した行にでくわすと、その行の先頭にブロックの終了を示す } を挿入する。
emacs の haskell-mode または xyzzy の hs-mode を使えばほぼ自動でインデントしてくれます。

8. チュートリアルへのリンク

これまでの説明で、とりあえず Haskell のプログラムを書いて 実行できるようになったと思います。

最後に主な(初心者向け) Tutorial へのリンクを張っておきます。