HOME もうひとつの Scheme 入門 書き込む

5. 分岐


1. 初めに

前回までで、とりあえず Scheme で関数を定義できるようになりました。 しかし、前回までの知識では、処理を分岐させることができないので、 関数電卓の域を出ないものでした。

今回は処理の分岐について説明します。

2. if 式

if は処理を2つに分岐させる形式です。 書式は以下の通りです。
(if predicate then_value else_value)
predicate (述語)がを返せば、then_value が、 そうでなければ else_value の値が評価されて括弧の外に出て行きます。 ここで、というのは偽 (#f で表される。) 以外の値全てです。真の代表値は #t です。

Scheme では偽を表す #f と、 空リストを表す '() は別のものです。 一方、Common Lisp では、偽と空リストは同じものとして規定されています。 Common Lisp から移ってきた人は両者を混同しないよう注意する必要があります。

条件を打ち消すときは not 関数を使います。 この関数は、引数が #f なら #t を、 それ以外なら #f を返します。

if は普通の関数と違い、引数を全て評価することをしません。 predicate が真のときは then_value だけを評価し else_value は評価しません。 その逆に、predicate が偽のときは else_value だけを評価し、 then_value は評価しません。 if の様にカッコ内のトークンの全てを評価しない手続きを特殊形式と呼びます。

例:初項 a0, 公比 r, 項数 n の等比数列の和

(define (sum-gp a0 r n)
  (* a0
     (if (= r 1)
	 n
	 (/ (- 1 (expt r n)) (- 1 r)))))   ; !!
一般に、等比数列の和は、
a0 * (1 - rn) / (1 - r)
で求まります。ただし、公比が 1 のときは、
a0 * n
になります。 if が引数全てを評価すると、r==1 の時も ; !! の部分が 評価されて、0 で割るエラーが発生します。

else_value は省略することができますが (注1)、 その場合は predicate が成り立たないとき返る値は規定されていません。 predicate が成り立たないとき #f を返したいときは、#f が返ることを明示する必要があります。

then_value, else_value も値を返す式1つだけを指定できます。 複数の式を指定したい場合には begin 式でくくる必要があります。 今のところは値を返す式だけを説明しているので、begin を使う必要はありません。begin については代入、I/O のところで 説明します。

注 1: MzScheme の場合は else_value を省略できません。

練習問題 1

次の関数を作ってください。条件式を作るときは
5 節を参考にして下さい。
  1. 実数の絶対値をを求める関数
  2. 実習の逆数を求める関数。引数が 0 のときは #f を返すようにしてください。
  3. 整数を ASCII 文字のうち図形文字に変換する関数。 図形文字に変換できる整数は 33 – 126 です。 整数を文字に変換するには integer->char 関数を使います。 変換できないときは #f を返すようにして下さい。

3. and と or

andor は条件式を結合するのに使います。 ただし、Scheme (Lisp) の and, or は 真偽値を返すだけではありません。値そのものが返ってきます。and, or を使いこなすと コードを短くすることができます。

3.1. and

and は任意個の引数をとり、引数を左から評価していき、最初に偽になったところでその値を返し、 残りの引数は評価しません。最後の引数が評価されたときはその値を返します。
> (and #f 0)
#f

> (and 1 2 3)
3

> (and 1 2 3 #f)
#f

3.2. or

or は任意個の引数をとり、引数を左から評価していき、 最初に真(#f 以外)になったところでその値を返し、 残りの引数は評価しません。最後の引数が評価されたときはその値を返します。
> (or #f 0)
0

> (or 1 2 3)
1

> (or #f 1 2 3)
1

> (or #f #f #f)
#f

練習問題 2

次の関数を作ってください。
  1. 与えられた3つの実数が全て正ならその積を返す関数
  2. 与えられた3つのうちのどれか1つが負ならその積を返す関数。

4. cond 式

分岐はすぺて if 式を用いて表すことができますが、 分岐が多岐にわたる場合、if 式を入れ子にしなければならず、 コードが読みにくくなります。 そのような時 cond 式を使うとすっきりと書けます。 cond 式の書式は以下の通りです。
(cond
  (predicate_1 clauses_1)
  (predicate_2 clauses_2)
    ......
  (predicate_n clauses_n)
  (else        clauses_else))
この式では、predicate_1 から順番に調べ、一致した条件式のところの節を評価します。 条件に一致しない節は評価されません。

clauses_i には複数の式を書くことができ、最後の式の値が返ってきます。 どの条件も成り立たないときは clauses_else の値が返ってきます。

(else clauses_else) を必ず書く癖をつけると、わかりにくいバグに悩まされることが減ります。 例:市営プールの料金
Foo 市の市営プールでは年齢によって料金を分けています。

市営プールの料金を計算する関数は以下のようになります。
(define (fee age)
  (cond
   ((or (<= age 3) (>= age 65)) 0)
   ((<= 4 age 6) 50)
   ((<= 7 age 12) 100)
   ((<= 13 age 15) 150)
   ((<= 16 age 18) 180)
   (else 200)))

練習問題 3

次の関数を作ってください。
  1. 試験の点数に応じて A--D の評価をつけます。点数を引数にとり、評価を返す関数を書いてください。
    1. 80 点以上 A
    2. 60 点以上 79 点以下 B
    3. 40 点以上 59 点以下 C
    4. 40 点未満 D

5. 便利な構文: begin, when, unless

5.1. begin

複数の式を1つにまとめるには begin という特殊形式を使います。 begin は与えられた式を前から順番に評価していき、最後の式の値を返します。
(begin s1 s2 ... s-end)

s1, s2 ... s-end を前から順番に評価し、s-end の値を返す。

例:

(define (foo)
  (begin
    (display "hello world.")
    (newline)
    (display "I love Scheme.")
    (newline)
    'done))
"hello world"、 "I love Scheme." が表示されて、値 done が返ります。
> (foo)
hello world.
I love Scheme.
done

5.2. when

when は R6RS のライブラリで定義されている構文です。条件を満たさなかった場合を記述する 必要がないときに使います。

(when predicate
    s1
    s2
    ...
    s-end )

predicate が成り立つとき、s1, s2 ... s-end を順番に評価し、s-end の値を返します。

例:

(define (foo fine)
  (when fine
    (display "hello world.")
    (newline)
    (display "It is fine, today.")
    (newline)
    'done))
> (foo #t)
hello world.
It is fine, today.
done

5.3. unless

unless は R6RS のライブラリで定義されている構文です。条件を満たした場合を記述する 必要がないときに使います。

(unless predicate
    s1
    s2
    ...
    s-end )

predicate が成りたたないとき、s1, s2 ... s-end を順番に評価し、s-end の値を返します。
例:
(define (foo cold)
  (unless cold
    (display "hello world.")
    (newline)
    (display "It is not cold, today.")
    (newline)
    'done))
> (foo #f)
hello world.
It is not cold, today.
done

6. 述語を作る関数

いくつかの述語を作る関数を紹介しておきます。 Scheme の真偽判定関数はほとんど '?' で終わる名前を持っています。

6.1. eq?, eqv?, equal?

eq?, eqv?, equal? はオブジェクトが等しいか調べるときの基本的な関数です。 以下に示すような違いがあります。
eq?
2つの引数をとり、2つのオブジェクトのアドレスを比較する。アドレスが等しければ真、 そうでなければ偽。下の例では、str のアドレスは自分自身のアドレスと等しいので真、一方、"hello" と "hello" は 別のアドレスに記憶されるので偽。 数値の比較では MIT-Scheme では同じ数値は真になるが、R5RS では規定されていないので、 互換性を考えて用いないのが無難です。数値の比較には eqv?= を用います。
> (define str "hello")
> (eq? str str)
#t
> (eq? "hello" "hello")
#f  

;;; 数値の比較は処理系依存
> (eq? 1 1)
#t

> (eq? 1.0 1.0)
#f
eqv?
2つの引数をとり、2つのオブジェクトの(アドレスに書かれている)データ型と値を比較する。データ型と値が等しければ真、 そうでなければ偽。文字列や lambda 式の比較は処理系依存。 文字列では、最初のアドレスに入っている値はその文字列そのものではないので、真になる保証はない (多くの処理系で偽になる)。 リストの場合も、最初のアドレスに入っているのはコンスセルで、一般に、cdr部には次のコンスセルへのアドレス が入っている(そしれそれは、ほとんどの場合異なる)ので偽になる。
> (eqv? 1.0 1.0)
#t
> (eqv? 1 1.0)
#f

;;; リストの比較では偽になる
> (eqv? (list 1 2 3) (list 1 2 3))
#f

;;; 文字列の比較は処理系依存 (文字列の比較には equal? または string=? を使うべき )
> (eqv? "hello" "hello")
#f

;;; 以下の例は処理系依存
> (eqv? (lambda(x) x) (lambda (x) x))
#f
equal?
リストや文字列を比較するときに用います。
> (equal? (list 1 2 3) (list 1 2 3))
#t

> (equal? "hello" "hello")
#t

6.2. データ型識別関数

以下に主なデータ型識別関数を挙げます。 すべて、1つの引数をとります。
pair?
コンスセルを含むオブジェクトの場合は真
list?
リストの場合は真。'() はリストであるが、pair では無いことに注意。
null?
'() の場合は真
symbol?
シンボルの場合は真
char?
文字の場合は真
string?
文字列の場合は真
number?
数値の場合は真
complex?
複素数の場合は真
real?
実数の場合は真
rational?
有理数の場合は真
integer?
整数の場合は真
exact?
浮動小数点以外は真
inexact?
浮動小数点のとき真

6.3. 数値比較関数

=, <, >, <=, >=
任意個の引数をとり、引数の間で、関数で示された関係が成り立っていれば真
> (= 1 1 1.0)
#t

> (< 1 2 3)
#t
> (< 1)
#t
> (<)
#t

> (= 2 2 2)
#t

> (< 2 3 3.1)
#t

> (> 4 1 -0.2)
#t

> (<= 1 1 1.1)
#t

> (>= 2 1 1.0)
#t

> (< 3 4 3.9)
#f
odd?, even?, positive?, negative?, zero?
1つの引数をとり、関数名で表される性質を満たしていれば真

6.4. 文字比較関数

文字比較関数には char=?, char<?, char>?, char<=?, char>=? があります。 詳しくは
R6RS を参照してください。

6.5. 文字列比較関数

文字列比較には string=?, string-ci=? などがあります。 詳しくは R6RS を参照してください。

7. 終わりに

今回は分岐について説明しました。分岐には if または cond 式を使います。 着実に本格的なプログラムに近づいてきました。

次回は局所変数について述べます。

練習問題の解答

練習問題 1

; 1
(define (my-abs n)
  (* n
     (if (positive? n) 1 -1)))
     
; 2     
(define (inv n)
  (if (not (zero? n))
      (/ n)))

; 3
(define (i2a n)
  (if (<= 33 n 126)
      (integer->char n)))

練習問題 2

; 1
(define (pro3and a b c)
  (and (positive? a)
       (positive? b)
       (positive? c)
       (* a b c)))

; 2
(define (pro3or a b c)
  (if (or (negative? a)
	  (negative? b)
	  (negative? c))
      (* a b c)))

練習問題 3

(define (score n)
  (cond
   ((>= n 80) 'A)
   ((<= 60 n 79) 'B)
   ((<= 40 n 59) 'C)
   (else 'D)))