M.Hiroi's Home Page

Functional Programming

お気楽 Scheme プログラミング入門

[ PrevPage | Scheme | NextPage ]

Scheme の基礎知識 [3]

前回は「変数」と「評価」について説明して、基本的なリスト操作を行う関数を紹介しました。今回は「条件分岐」と「再帰定義」について説明します。

●条件分岐

条件分岐は難しい話ではありません。たとえば、「暑いからアイスコーヒーを飲もう」とか、「雨が降りそうだから傘を持っていこう」というように、私たちはそのときの状況によって自分の行動を決めています。プログラミングの世界でも、状況によって次に実行する処理を選択しなければならないことがよくあります。これを「条件分岐」といいます。

先ほどの例では、「暑いから・・・」と「雨が降りそうだから・・・」が「条件」を示しています。ただし、このままでは条件部分があいまいなので、もっと具体的に条件部を書き換えてみます。

  「もしも気温が 30 ℃以上であれば、アイスコーヒーを飲む」
          ~~~~~~~~~~~~~~~~

  「もしも降水確率が 50 %以上であれば、傘を持っていく」
          ~~~~~~~~~~~~~~~~~~~~

これだと条件部がよくわかると思います。そして、実際に「気温が 30 ℃以上になる」ことを「条件を満たす」または「条件が成立する」といいます。条件が成立したときに「アイスコーヒーを飲む」という行動が実行されます。降水確率が 40 %であれば、条件が成立しないので、「傘を持っていく」という行動は実行されません。

これを図に示すと、次のようになります。


                   図 : 条件分岐

(1) では、「もしも条件を満たすならば、処理 A を実行する」となります。この場合、条件が成立しないと処理は何も実行されませんが、(2) のように条件が成立しない場合でも処理を実行することができます。(2) の場合では、「もしも条件を満たすならば、処理 A を実行し、そうでなければ処理 B を実行する」となります。すなわち、条件によって処理 A か処理 B のどちらかが実行されることになります。

プログラミングの世界では、条件が成立することを「真 (true)」といい、条件が不成立のことを「偽 (false)」といいます。実際のプログラミングでは、true と false を表すデータが必要になります。Scheme の場合、真偽を表すデータ型 (boolean) として #t (真) と #f (偽) が用意されています。Scheme 処理系では #f を偽と判断し、それ以外の値を真と判断します。#t は真を表すデータの代表として使います。

それでは、Scheme で条件分岐を表してみましょう。Scheme には、条件分岐を実行する関数がいくつかありますが、いちばん簡単な関数が if です。if は英語で「もしも」という意味ですから、まさに条件分岐そのものを表しています。if の基本的な使い方は次のようになります。

(if <条件部> <処理A> <処理B>)

if は 3 つの引数を受け取りますが、シンタックス形式なので評価されずにそのまま if に渡されます。最初に、if は <条件部> を評価します。この評価結果が #f 以外の値であれば、条件を満たしていると判断し、処理 A を評価します。この場合、処理 B は評価されません。評価結果が #f であれば、処理 B を評価します。この場合、処理 A は評価されません。また、条件部が成立したときに実行する処理を「then 節」、不成立のときに実行する処理を「else 節」といいます。

●述語

Lisp / Scheme では、条件部で使用する関数を「述語 (predicate)」といいます。述語は真か偽を返す関数です。先ほど説明したように、#f 以外の値は真と判断されるのですが、述語では条件を満たす場合は #t を返します。#t は真を表す代表選手なのです。

それでは、実際にプログラミングしてみましょう。「もしも気温が 30 ℃以上であれば、アイスコーヒーを飲む」をプログラムしてみましょう。

リスト : 飲物を選ぶプログラム

(define (select-drink degree)
  (if (<= 30 degree)
      (display "Drink ice coffee\n")
      (display "Don't drink ice coffee\n")))

関数名や変数名には、その機能を表す名前をつけるようにしましょう。この場合、名前は selectdrink と続けて書くよりも、間に '-' (ハイフン) を含めたほうが、一目でわかるようになります。関数名にハイフンを使うことができないプログラミング言語では、'_' (アンダーライン) を使って select_drink と書くことで同様の効果を得ることができます。

このほかに SelectDrink や selectDrink のように大文字を使う場合もあります。一般に、Lisp / Scheme では単語と単語の間をハイフンで繋ぐことが多いようです。

select-drink は、気温 degree によって飲物を選びます。<= は数値を比較する述語です。右側の引数が左側の引数以上であれば #t を返し、そうでなければ #f を返します。ここで数値を比較する述語をまとめて紹介しましょう。

  1. = N1 N2 N3 ... ==> (N1 = N2 = N3 = .... )
    引数がすべて等しければ #t を、それ以外であれば #f を返す。
  2. < N1 N2 N3 ... ==> (N1 < N2 < N3 < .... )
    引数を左から見て、単調に増加していれば #t を、それ以外であれば #f を返す。
  3. > N1 N2 N3 ... ==> (N1 > N2 > N3 > .... )
    引数を左から見て、単調に減少していれば #t を、それ以外であれば #f を返す。
  4. <= N1 N2 N3 ... ==> (N1 ≦ N2 ≦ N3 ≦ .... )
    引数を左から見て、単調に減少していなければ #t を、それ以外であれば #f を返す。
  5. >= N1 N2 N3 ... ==> (N1 ≧ N2 ≧ N3 ≧ .... )
    引数を左から見て、単調に増加していなければ #t を、それ以外であれば #f を返す。

これらの述語は、右側に書いた数式の関係を満たせば #t を返し、そうでなければ #f を返します。引数は 2 つ以上与えてもかまいません。数値は整数値以外にも浮動小数点数、有理数、複素数を使うことができます。

それでは実際に select-drink を実行してみましょう。

gosh[r7rs.user]> (select-drink 35)
Drink ice coffee       <== display による画面表示
#<undef>               <== select-drink の返り値
gosh[r7rs.user]>

まず、述語 (<= 30 degree) が評価されます。degree は 35 なので条件を満たし、then 節である (display "Drink ice coffee\n") が評価されます。display の副作用により画面に Drink ice coffee が表示され、display の返り値が if の返り値となり、それが select-drink の返り値となって #<undef> が表示されます。

●複数の述語を組み合わせる

複数の述語を組み合わせる場合はシンタックス形式 and と or を使います。

(and S式1 S式2 S式3 S式4 ..... )
(or  S式1 S式2 S式3 S式4 ..... )

and は複数の述語を「~かつ~」で結ぶ働きをします。and は与えられた S 式を左から順番に評価します。そして、S 式の評価結果が #f であれば、残りの S 式を評価せずに #f を返します。最後まで S 式が #f に評価されなかった場合は、いちばん最後の S 式の評価結果を返します。

or は複数の述語を「~または~」で結ぶ働きをします。or は and と違い、S 式の評価結果が #f 以外の場合に、残りの S 式を評価せずにその評価結果を返します。すべての S 式が #f に評価された場合は #f を返します。それでは、簡単な例を示しましょう。

gosh[r7rs.user]> (define (check-number x) (and (< 10 x) (<= x 20)))
check-number
gosh[r7rs.user]> (define (check-number-else x) (or (>= 10 x) (> x 20)))
check-number-else
gosh[r7rs.user]> (check-number 15)
#t
gosh[r7rs.user]> (check-number-else 15)
#f
gosh[r7rs.user]> (check-number 30)
#f
gosh[r7rs.user]> (check-number-else 30)
#t

最初の例は、引数 x が 10 より大きくて、かつ 20 以下の場合 #t を返します。次の例はその逆で、x が 10 以下、または 20 より大きい場合 #t を返します。

check-number と check-number-else は逆の関係です。いちいち 2 つの関数を作らなくても結果を逆にすればいいはずです。この場合は not を使います。not は引数が #f ならば #t を返し、それ以外ならば #f を返す関数です。つまり、真と偽をひっくり返す働きをします。これを「否定」といいます。ひらたくいえば、「~ではない」という意味になります。次の例を見てください。

gosh[r7rs.user]> (check-number 15)
#t
gosh[r7rs.user]> (not (check-number 15))
#f

このように not を使うことで、述語の判定を逆にすることができます。

●when と unless

関数 if は条件部が不成立になったときに実行する処理を省略することができます。

リスト : 飲物を選ぶプログラム

(define (select-drink1 degree)
  (if (<= 30 degree)
      (display "Drink ice coffee\n")))

条件部が不成立で実行する処理がない場合、Gauche は #<undef> を返します。

if の else 節が存在しない場合には、if の代わりに when を使うことができます。逆に、if の then 節で評価する処理が無い場合には unless を使うことができます。when と unless は R7RS-small で使用することができます。R5RS には定義されていません。ご注意ください。

(when test S式1 S式2 S式3 ..... )

when は最初に test を評価しその結果が #t であれば、S 式を順番に評価して最後の S 式の評価結果を返します。test の結果が #f ならば、その後ろの S 式は評価しません。このときの返り値は未定義です。Gauche は #<undef> を返します。

unless は when とは逆の働きをします。述語が偽 (#f) に評価されたときに、引数の S 式を順番に評価します。unless は述語 not を使うと、次のように whenを使って表すことができます。

(unless test S式1 ...) ≡ (when (not test) S式1 ...)

if の場合、then 節や else 節はひとつの S 式しか受け付けませんが、when やunless では、複数の S 式を引数として受け取ることができます。

●数と算術演算

ここで、Scheme で用意されている数と算術演算についてまとめておきましょう。Scheme では、整数 (integer)、有理数 (rational)、浮動小数点数 (floating-point number)、複素数 (complex number) という 4 種類の数があります。Gauche でもこれらの数を使うことができます。

●整数

Scheme の場合、整数の大きさには制限がありません。これは Gauche も同じで、5000! や 10000! でも求めることができます。整数は通常 10 進表記ですが、次に示す基数でも書くことができます。

簡単な例を示しましょう。

#b10101010 => 170
#o1234567  => 342391
#o-127     => -87
#xabcdef   => 11259375

●有理数

Scheme は有理数 (分数) を扱うことができます。有理数は 2 つの整数を / で区切って表します。簡単な例を示します。

1/2, 2/3, 4/3, 11/13, -51/100, 30517578125/32768

また、4/6 や 3/12 のような入力もできますが、この場合は約分されることになります。とくに、4/2 のような割り切れる有理数は、ただちに整数に変換されます。次の例を見てください。

4/6  => 2/3
3/12 => 1/4
10/5 => 2    ; 整数に変換される

●浮動小数点数

Scheme は浮動小数点数を扱うことができます。Scheme の場合、浮動小数点数は一種類だけではなく、短精度 (short)、単精度 (single)、倍精度 (double)、長精度 (long) という 4 つの形式がありますが、すべてをサポートしなければならないわけではなく、処理系によっては 4 つより少なくてもかまいません。Gauche ユーザリファレンス 6.3 数値 によると、『実装に使われるC言語のdouble型で表現されます。通常IEEE 64bit浮動少数点数です。』 とのことです。

浮動小数点の表記法は次のようになります。

[+|-] 数字 小数点 数字 指数マーカ [+|-] 数字

例: 0.1234, 1.2345e10, -9.876542999999999e-100

なお、指数マーカには s (短精度), f (単精度), d (倍精度), l (長精度), e (デフォルト精度) を使うことができますが、Gauche の浮動小数点数は 1 種類 (double) しかないので、どれを指定しても倍精度の浮動小数点数になります。

●複素数

Scheme は複素数を扱うことができます。形式は "数+数i" または "数-数i"です。最初の数が実部で、虚部の数は後ろに i を付けて表します。簡単な例を示しましょう。

5-3i => 5.0-3.0i
1.2+2.4i => 1.2+2.4i
1/2+1/4i => 0.5+0.25i

実部と虚部の指定には、整数、浮動小数点数、有理数を使うことができますが、Gauche では浮動小数点数に変換して扱います。

●算術演算

次は簡単な算術演算を説明します。

+ は足し算を、* は掛け算を、- は引き算を行います。これらの関数は引数をいくつでも取ることができます。数以外のデータを引数に与えるとエラーになります。引数の型が異なる場合は強制的に型変換が行われます。簡単な例を示しましょう。

(+)           => 0
(+ 1)         => 1
(+ 1 2 3)     => 6
(+ 1 2 3 1/2) => 13/2
(+ 1 2 3 4.5) => 10.5

(*)           => 1
(* 1)         => 1
(* 1 2 3)     => 6
(* 1 2 3 1/4) => 3/2
(* 1 2 3 4.5) => 27.0

(- 1)         => -1
(- 10 5 4)    => 1
(- 10 5/2)    => 15/2
(- 10 4.5)    => 5.5
(-)           => Error  ; 引数が足りない

/ は割り算を行います。整数同士の割り算で割り切れない場合は分数になります。0 で割り算したとき、引数が整数の場合はエラーになります。引数が実数の場合、Gauche は +inf.0 または -inf.0 という無限大を表すデータを返します。

(/ 2)      => 1/2    ; 引数の逆数を求める
(/ 8 4 2)  => 1      ; 約分されて整数になる
(/)        => Error  ; 引数が足りない
(/ 1 0)    => Error  ; 0 で除算した場合
(/ 1.0 0)  => +inf.0
(/ -1 0.0) => -inf.0

2 つの整数 n1 と n2 の商は関数 quotient で、剰余は関数 remainder, modulo で求めることができます。

(quotient 4 2)   => 2
(remainder 4 2)  => 0
(modulo 4 2)     => 0

(quotient 5 2)   => 2
(remainder 5 2)  => 1
(modulo 5 2)     => 1

(quotient -5 2)  => -2
(remainder -5 2) => -1
(modulo -5 2)    => 1

(quotient 5 -2)  => -2
(remainder 5 -2) => 1
(modulo 5 -2)    => -1

quotient は n1 / n2 が割り切れない場合、0 の方向へ丸めた値を返します。remainder は n1 / n2 の剰余で、その符号は n1 と同じになります。modulo も n1 / n2 の剰余ですが、その符号は n2 と同じになります。

R7RS-small の場合、これらの関数は後方互換性のために用意されています。quotient および remainder はそれぞれ truncate-quotient および truncate-remainder と同等であり、modulo は floor-remainder と同等です。

このほかにも、Scheme には数値演算に関する関数がたくさん定義されています。興味のある方は Scheme の仕様書 R7RS-small や Gauche ユーザリファレンス: 6.3 数値 をお読みください。

●再帰定義

条件分岐が理解できると、同じ処理を何回も繰り返すプログラムを書くことができるようになります。まずは簡単な例として、階乗を計算するプログラムを考えてみます。

階乗の定義
\(\begin{array}{l} 0! = 1 \\ n! = n \times (n - 1)! \end{array}\)

階乗の定義からわかるように、n の階乗を求めるには (n - 1) の階乗の答えがわかれば求めることができます。実は、これをそのままプログラミングすることができるのです。

リスト : 階乗を計算する

(define (fact n)
  (if (= n 0)
      1
      (* n (fact (- n 1)))))

本当にこれで階乗を計算できるのか、実際に試してみると次のような計算結果となります。

n = 0 => 1
n = 1 => 1
n = 2 => 2
n = 3 => 6
n = 4 => 24
n = 5 => 120
n = 6 => 720
n = 7 => 5040
n = 8 => 40320
n = 9 => 362880

確かに階乗の答えを求めることができるようです。それでは、関数の処理内容について詳しく見ていきましょう。

関数 fact の内容は、「もしも n が 0 ならば 1 を返し、そうでなければ、(* n (fact (- n 1))) を実行する」というものです。条件が成立する場合が、0! = 1 を表していることは直ぐに理解できると思います。問題は条件が不成立になる場合です。

(* n (fact (- n 1))) では、n * (n - 1)! を計算しています。この S 式で注目すべき点は、(n - 1)! を求めるときに、関数 fact 自身を再び呼び出しているところです。これを「再帰定義 (recursive definition)」とか「再帰呼び出し (recursive call)」といいます

関数の定義に自分自身を使うことができるなんて、何か特別な仕掛があるのではないかと思われるかもしれません。筆者が最初に再帰定義を見たときは、ヘビが自分の尻尾を食べていくような奇妙な感覚に陥って、なかなか理解できませんでした。

ところが、再帰定義は特別なことではありません。ましてや、Lisp / Scheme の専売特許でもないのです。C言語や Pascal など近代的なプログラミング言語であれば、再帰定義を使うことができるのです。残念なことですが、Lisp / Scheme などの関数型言語以外では、再帰定義は難しいテクニックのひとつと思い込んでしまい、初心者の方は避けて通ることが多いように思います。

実は、再帰呼び出しは、今まで説明した関数の呼び出しとまったく同じなので、難しく考える必要はないのです。とくに Scheme の場合、再帰定義を積極的に活用してプログラミングを行うので、初心者の方が覚えるべき基礎テクニックのひとつにすぎません。慣れるまでちょっと苦労するかもしれませんが、ポイントさえつかめば簡単に使いこなすことができるようになります。

それでは、このプログラムの動作を説明します。


             図 : 再帰呼び出し

fact に 4 を与えて呼び出してみましょう。引数 n には 4 が代入されます。この引数 n の値は (fact 4) が実行されている間有効です。上図では、そのことを枠で示しています。この場合、n は 0 ではないので、else 節が実行されます。最初の枠の中を見てください。ここで n の値から 1 引いた値で自分自身を呼び出しています。

次に、2 番目の枠の中を見てください。引数 n には 3 が代入されます。ここで、関数を実行するときの動作を思い出してください。Scheme 処理系は、引数 n に対応するメモリ領域を割り当て、その領域に実引数を書き込み、関数本体を実行するのでしたね。再帰呼び出しであっても、普通の関数呼び出しには違いありません。

したがって、(fact 3) を実行する場合も、引数 n に対応するメモリ領域を割り当て、そこに実引数である 3 を代入します。つまり、(fact 4) と (fact 3) の呼び出しでは、引数 n に割り当てられるメモリ領域は異なっているのです。ここが再帰呼び出しを理解するポイントのひとつです。

プログラムを見ると引数 n はひとつしかないように見えますが、関数を呼び出すたびに、局所変数は新しいメモリ領域に割り当てられていくのです。したがって、(fact 4) を呼び出したときの n の値が更新されるのではなく、(fact 4) を実行しているときの n は 4 で、(fact 3) を実行しているときの n は 3 なのです。再帰呼び出しが行われるたびに、新しい変数 n がメモリ領域に割り当てられていくと考えてください。

同様に (fact 2), (fact 1), (fact 0) と順番に呼び出していきます。(fact 0) の場合、n は 0 ですから then 節が実行されます。ここで再帰呼び出しが止まります。ここが第 2 のポイントです。

停止条件を作らなかったり、作ってもその条件を満たさないならプログラムは正常に動作しません。停止条件がなかったり条件を満たさない場合、関数呼び出しが限りなく行われるので、Scheme のシステム資源 (メモリ) を食い潰し、いつかはプログラムの実行ができなくなります。

(fact 0) は 1 を返します。実行が終了したので、引数 n の値は元に戻ります。これも局所変数の特徴でしたね。では、どの値に戻るのでしょうか。関数を呼び出した場合、それを呼び出した関数に必ず戻ってきます。この場合は (fact 0) が終了したので、(fact 1) の実行に戻るのです。(fact 1) の実行は終了していないので、引数 n の値は 1 に戻ります。図でいえば、実行が終了したら枠が壊れていくと考えてください。いちばん内側の枠が壊れて、その前に値を代入された局所変数が顔を出すのです。

(fact 0) が 1 を返してきたので、(fact 1) を計算することができます。(* 1 1) を評価して (fact 1) は 1 を返します。同様に、順番に値を計算していき、最後に (fact 4) の値を求めることができるのです。

●再帰定義とリスト操作

基本的なポイントを押さえたら、あとは「習うより慣れろ」です。そこで、再帰定義を使って、リスト操作を行う関数を作ってみましょう。実は、リスト操作と再帰定義は相性が良いのです。これが Lisp / Scheme で再帰定義が頻繁に使われる理由でもあります。

それでは手始めに、リストの n 番目の要素を求めるプログラムを作ってみましょう。Lisp / Scheme の場合、リストの要素は 0 から数えます。したがって、リストの先頭の要素は 0 番目の要素となります。Scheme には list-ref という関数がありますが、私たちでも簡単にプログラムすることができます。関数名は retrieve とし、引数に数値 n とリスト ls を与えます。

リスト ls の 0 番目の要素を求めることは簡単に実現できます。リストの先頭の要素を取り出す関数 car を適用すればいいだけです。それでは、n 番目の要素を求めるにはどうしたらいいのでしょうか。実はこれも簡単です。階乗の計算を思い出してください。n! を求めるには (n - 1)! がわかれば十分でした。リスト ls の n 番目の要素は、ls の先頭の要素を取り除いたリストの n - 1 番目の要素がわかればいいのです。

再帰定義の場合、複雑な問題を簡単な問題へ置き換えるように考えていきます。階乗の場合は、n! が 0! まで簡単になりましたね。この場合も、n 番目の要素を 0 番目の要素になるように再帰させるのです。これをプログラムすると次のようになります。

リスト : リストの n 番目の要素を取り出す

(define (retrieve ls n)
  (if (zero? n)
      (car ls)
      (retrieve (cdr ls) (- n 1))))

関数 zero? は引数が 0 かチェックする述語です。Scheme の場合、述語には ? マークを付ける習慣があります。述語 zero? は (= 0 n) と同じ働きをします。

処理内容は簡単です。もしも n が 0 ならば、リスト ls に car を適用して先頭の要素を返します。これが再帰呼び出しの停止条件になります。そうでなければ、retrieve を再帰呼び出しします。このとき、リスト ls に cdr を適用して先頭の要素を取り除き、n の値を -1 します。

それでは実際に動かしてみましょう。

gosh[r7rs.user]> (retrieve '(a b c d) 0)
a
gosh[r7rs.user]> (retrieve '(a b c d) 1)
b
gosh[r7rs.user]> (retrieve '(a b c d) 2)
c
gosh[r7rs.user]> (retrieve '(a b c d) 3)
d

正常に動作していますね。

次は、前回紹介したリストとリストを結合する関数 append を作ってみましょう。関数名は my-append とし、引数としてリスト xs と ys を渡して、それを結合したリストを返します。

再帰に慣れていないと、どうしたらよいのか見当もつかないかもしれません。retrieve のときと同様に、簡単な場合から考えていきましょう。まず、リスト xs が空リスト () ならば、リスト ys を返すだけでいいですね。次に、リスト xs に要素がひとつしかない場合を考えてみます。これはリスト xs に car を適用して要素を取り出し、それを cons でリスト ys の先頭に追加すればいいでしょう。

ここでちょっと考えてみてください。xs が空リストの場合は ys をそのまま返しますよね。それでは、「リスト ys の先頭に追加する」という処理は、「空リストとリスト ys を結合したリストの先頭に追加する」と置き換えることができるはずです。リスト xs に cdr を適用すれば空リストになりますから、この処理は再帰定義で実現できるはずです。

つまり、リスト xs とリスト ys を結合するには、リスト xs の cdr とリスト ys を結合したリストに、リスト xs の car を cons すればいいのです。これを図に示すと次のようになります。


                 図 : my-append の動作

これをプログラムすると次のようになります。

リスト : リストの結合

(define (my-append xs ys)
  (if (null? xs)
      ys
      (cons (car xs) (my-append (cdr xs) ys))))

関数 null? は、引数が空リストであれば真を返す述語です。null? のようにデータ型をチェックする述語を「型述語」といいます。このほかにも、次に示すような型述語があります。

いずれの述語も引数をひとつ取り、引数がテスト条件を満たせば #t を、そうでなければ #f を返します。

プログラムに戻りましょう。if で xs が空リストかチェックし、そうであればリスト ys をそのまま返します。そうでなければ、リスト xs に cdr を適用した値を my-append に渡して再帰呼び出しします。その結果とリスト xs の car を cons で接続すればいいのです。それでは、実際に実行してみましょう。

gosh[r7rs.user]> (my-append '(a b) '(c d))
(a b c d)
gosh[r7rs.user]> (my-append '((a b) (c d)) '((e f) (g h)))
((a b) (c d) (e f) (g h))

正常に動作していますね。

●まとめ

今回はここまでです。最後に、今まで説明したことについて、簡単に復習しておきましょう。

  1. 条件分岐には if を使う。
  2. R7RS-small では when と unless も使用できる。
  3. 述語は真か偽を返す関数である。
  4. 述語の組み合わせは and (論理積) と or (論理和) を使う。
  5. not (否定) は真偽を逆にする。
  6. =, <, >, <=, >= は数値を比較する述語である。
  7. +, -, *, / は算術演算を行う関数である。商は quotient で、剰余は remainder, modulo で求める。
  8. 再帰は関数にそれ自身の呼び出しを許す。
  9. リスト操作は再帰定義を使うと簡単にプログラムできる。
  10. null?, number?, symbol?, string?, list?, pair? などはデータ型の述語である。

次回は「局所変数の定義」と「繰り返し (末尾再帰)」について説明します。お楽しみに。

●問題

次の関数を定義してください。

  1. 引数 n が引数 m の約数か判定する述語 divisor? m n
  2. 引数 n が 3 または 5 の倍数か判定する述語 three-or-five-multiple n
  3. 引数 n が引数 low, high の範囲内にあるか判定する述語 between n low high
  4. リストの要素がただひとつか調べる述語 single? xs
  5. リストの要素が二つあるか調べる述語 double?












●解答

gosh[r7rs.user]> (define (divisor? m n) (zero? (modulo m n)))
divisor?
gosh[r7rs.user]> (divisor? 15 3)
#t
gosh[r7rs.user]> (divisor? 15 5)
#t
gosh[r7rs.user]> (divisor? 15 7)
#f

gosh[r7rs.user]> (define (three-or-five-multiple n) (or (divisor? n 3) (divisor? n 5)))
three-or-five-multiple
gosh[r7rs.user]> (three-or-five-multiple 9)
#t
gosh[r7rs.user]> (three-or-five-multiple 10)
#t
gosh[r7rs.user]> (three-or-five-multiple 11)
#f

gosh[r7rs.user]> (define (between n low high) (<= low n high))
between
gosh[r7rs.user]> (between 5 1 10)
#t
gosh[r7rs.user]> (between 0 1 10)
#f
gosh[r7rs.user]> (between 20 1 10)
#f

gosh[r7rs.user]> (define (single? xs) (and (pair? xs) (null? (cdr xs))))
single?
gosh[r7rs.user]> (single? '(a))
#t
gosh[r7rs.user]> (single? '(a b))
#f
gosh[r7rs.user]> (single? '())
#f

gosh[r7rs.user]> (define (double? xs) (and (pair? xs) (single? (cdr xs))))
double?
gosh[r7rs.user]> (double? '(a b))
#t
gosh[r7rs.user]> (double? '(a b c))
#f
gosh[r7rs.user]> (double? '(a))
#f

初版 2007 年 12 月 23 日
改訂 2020 年 8 月 30 日

Copyright (C) 2007-2020 Makoto Hiroi
All rights reserved.

[ PrevPage | Scheme | NextPage ]