第7回 制御構造(5月21日;5月29日)

今日の課題

■ lambda 式(ラムダ式)

□ 関数の作成(その1)

Scheme で、関数を作成するために、lambda 式を使用する。

lambda 式は、特殊形式の式である。その形式は以下のとおりとなる。
(lambda (引数1 引数2 … 引数n) 式1 式2 … 式m)

最左部分式に「lambda」と記載する。次に括弧を書いてその中に仮引数となる変数名を列挙する。その次に、関数での計算手順を列挙する。計算が複雑な場合などに対して、複数の式を並べて良い。関数の返り値は、最後の式(式m)の評価結果である。

◎例1

与えられた値 x を3倍して返す関数を作ろう。
(lambda (x) (* x 3))

※ 定義エリアで上記を記述し、実行(Run)すると、結果が対話エリアに出力される。以下の例も同様。

□ 関数の使用(その1)

例1を実行してみると、#<procedure>が返される。 lambda 式を評価した結果として、作成された関数が返り値となっていることが分かる。

返り値が「procedure」となるものは、コンビネーションの最左部分式に置くことで、使用することができる。

◎例2

例1の関数を使ってみよう。
((lambda (x) (* x 3)) 6)

この例は、全体を見るとコンビネーション ( 式1 式2 ) という形式であり、式1が例1の式、式2が定数6となっている。

□ 関数の作成(その2)

例2では、先に作成したlambda 式の関数を、使う段階で再度書かなければならなかった。何度も使用される関数には名前を付けておきたい。そのためには、defineを使用する。

◎例3

例1の関数に「sanbai」という名前を付けよう。
(define sanbai (lambda (x) (* x 3)))

□ 関数の使用(その2)

例3で付けた名前を、コンビネーションの最左部分式のところに記載することで、関数が使用できる。

◎例4

例3の関数を用いて計算をしてみよう。
(sanbai 7)

■ 順接

lambda 式では、複数の式を並べることができる。

◎例5

与えられた値3つ x y z の分散を返す関数を作ろう。
(lambda (x y z)
    (define m1 (/ (+ x y z) 3))
    (define m2 (/ (+ (* x x) (* y y) (* z z)) 3))
    (- m2 (* m1 m1)))

◎練習1

(1) 上記の関数を直接用いて、3つの値「3、4、5」の分散を求めよ(例2の方法で実行)。

(2) 上記の関数に「variance3」という名前を付けよう。

(3) variance3 を用いて、3つの値「6、7、8」の分散を求めよ(例3の方法で実行)。

上記の例では、「m1、m2」という名前を使ったが、C言語でいうところのローカル変数に似ている。

◎発展

以下のプログラムを見てみよう。3つの値の平均を求める関数を「average3」としている。この名前をラムダ式の中だけで使うことができる。さらに、2乗の関数を「pow2」としている。これらは、ローカルな関数といえる。Smalltalk でのブロックに似ている。
(lambda (x y z)
    (define average3             ; 3値の平均値を求める関数を定義
        (lambda (a b c)          ;
            (/ (+ a b c) 3)))    ;
    (define pow2                 ; 2乗を求める関数を定義
        (lambda (a)              ; 変数 a は lambda 式内でローカルなので、
            (* a a)))            ; average3 の a と区別される。
    (define m1 (average3 x y z))
    (define m2 (average3 (pow2 x) (pow2 y) (pow2 z)))
    (- m2 (pow2 m1)))

■ 条件式

条件式のための特殊形式としてifがある。次の2つの書式がある。
(if 〈真偽を返す式〉 〈真の際に評価する式〉 〈偽の際に評価する式〉)
(if 〈真偽を返す式〉 〈真の際に評価する式〉)

ここで、2つ目の書式において、「真偽を返す式」が偽となった際に評価する式が無い。したがって、返り値は不明である。しかし、処理はエラー終了とならずに進む。

Lispにおける伝統的な条件式として、cond がある。
(cond (〈真偽を返す式1〉 〈真偽を返す式1真の際に評価する式の並び1〉)
      (〈真偽を返す式2〉 〈真偽を返す式2真の際に評価する式の並び2〉)
                  - - - 
      (〈真偽を返す式n〉 〈真偽を返す式n真の際に評価する式の並びn〉))

〈真偽を返す式n〉の位置に、else または #t を記載することで、「その他の場合」、たとえば、C言語での default に相当するものが記述できる。

◎練習2

引数の値が、正ならば 'plus を、負ならば 'minus を、それ以外ならば 'zero を返す関数を作成せよ。

(1) if を用いて作成したものを sign-if と名前を付けよ。

(2) cond を用いて作成したものを sign-cond と名前を付けよ。

答えはここ⇒

■ 再帰呼出し

ある名前で定義した関数の中で、その名前の関数を評価することを再帰呼出しという。

例6

階乗を計算する関数 factorial を定義しよう。factorial では再帰を終了する条件は、0 の階乗を求めるときであり、その返り値は、1 である。
(define factorial
    (lambda (x)
        (if (= x 0)
            1
            (* x (factorial (- x 1))))))

■ 小レポート

以下の関数について scheme プログラムを作成せよ。プログラムを印刷し、各行に手書きで説明を書いて、提出せよ。そして、実行の様子をスクリーンショットで画像化し、様子を手書きで説明して、提出せよ。

問1

(1) ある事象の生起する確率 p を入力し、その事象の自己情報量を返す関数 self-information を作成せよ。なお、自己情報量は - log2 p で計算する。DrScheme の log 関数は底が e であるので、底を 2 で計算するようにしておこう。

(2) ある事象 a が事象 b の生起という条件付きで生起する確率 pij および事象 a の生起する確率 pi を入力し、事象 a と b の相互情報量を返す関数 mutual-information-c を作成せよ。なお、条件付き確率を用いた相互情報量の計算式は、log2( pij / pi ) である。

問2

成績を入力して、ABCラベルを出力する関数を作成しよう。もし、成績 s が 90以上ならば 'A、80以上90未満ならば 'B、70以上80未満ならば 'C、60以上70未満ならば 'D、60未満ならば 'E をそれぞれ返り値とせよ。

(1) if を使う場合を、label1 という名前を付けて、作成しよう。

(2) cond を使う場合を、label2 という名前を付けて、作成しよう。

問3

n人の人物が集まったとき、そのうち少なくとも誰か2人が同じ誕生日である確率を計算する関数 p-same-day を作成しよう。ちなみに、(p-same-day 23)は、小数に変換すると、約 0.507 となる。ただし、プログラム内で再帰呼び出しを使うこととし、コメントによりその部分を説明せよ。
(実行例)
(exact->inexact (p-same-day 23))    ; exact->inexact で、分数を小数に変換
0.5072972343239853

ヒント
a) この計算には、n人が全て異なる誕生日である確率を 1 から引けば良い。すなわち、全て異なる誕生日である確率を求める関数を p-diff-day としておき、(- 1 (p-diff-day n)) を p-same-day で計算すればよい。
b) たとえば、n = 3 人の場合に、3 人とも異なる誕生日であるという状況は、

365 * (365 - 1) * (365 - 2) (通り)

が存在する。全ての誕生日の状況は、365 * 365 * 365 通りであるので、3 人が全て異なる誕生日である確率は、

365 * (365 - 1) * (365 - 2) / 365 * 365 * 365

である。n人の場合の確率を一般式で表わせば次のようになる。

1 * (1 - 1/365) * (1 - 2/365) * … * (1 - (n - 1)/365)

そこで、この式に従った再帰呼び出しを利用して、全員が異なる誕生日である確率を求める関数を作成すればよい。


(c) 2009.5.18 by tokuhisa