Guile基礎/条件分岐

変更履歴

概 要

目 次

参考資料

if 式

if式は次の文法に沿って記述します.
(if test consequence alternate) syntax
(if test consequence) syntax
   test         ::=  expression
   consequence  ::=  expression
   alternate    ::=  expression
   expression   ::=  式

if 式は次のように評価されます.まず判定式(test)が評価されます. その結果が真値(#f 以外の値)だったとき, 帰結式(consequence)が評価され, その結果が if 式の評価結果として返されます. また,このとき,代替式(alternate)は無視されます. 一方,判定式(test)の結果が #f だった場合,代替式(alternate)が評価され, その結果が if 式の評価結果として返されます. このとき,帰結式(consequence)は無視されます. 判定式(test)の結果が #f で代替式(alternate)がない場合,if 式の結果は unspecified です.

cond 式

cond式は判定式(下記のtest)の結果に基づいて条件分岐(場合分け)を行うための構文です.cond 式は次の文法に沿って記述します.
(cond cond-clause+) syntax
(cond cond-clause* else-clause) syntax
   cond-clause  ::=  (test expression+)
                    |  (test)
                    |  (test => recipient)
                    |  (test guard => recipient)
   else-clause  ::=  (else expression+)
   test         ::=  expression
   expression   ::=  式
   recipient    ::=  1引数の手続き
   guard        ::=  手続き

cond 式は,おおまかに見ると

(cond
 (test$_1$ ...)
  ......
 (test$_n$ ...)

または

(cond
 (test$_1$ ...)
  ......
 (else ...)

といった形式をしています.Guile はこのcond式を次のように処理します. 以下では,cond節やelse節の評価の仕方を説明します. そこで,上の説明の中で選んだcond節またはelse節を

 (test$_k$ ...)  または   (else ...)

とおきます.

(test$_k$ expression+) または (else expression+)

このパターンのcond節またはelse節を選んだとき, 帰結部(expression+)の式を先頭から順に評価していって, 最後の式の結果をcond式全体の評価結果として返します.

(test$_k$)

このパターンのcond節を選んだとき,test$_k$ の結果をcond式の評価結果として返します.

(test$_k$ => recipient)

このパターンのcond節を選んだとき,test$_k$ の結果を実引数として recipient を実行し,その返り値をcond式全体の評価結果として返します.次の点に注意して下さい.

(test$_k$ guard => recipient)

このパターンはGuile固有の機能なのですが,上の3つのcond節とは動作が大きく異なります.なによりもまず,判定式(test$_k$)の真偽は無視され,test$_k$ の値を実引数として guard を実行してしまいます.そのため, このcond節を選ぶか否かは判定式(test$_k$)だけでは決まらず,guard の結果によって決まります.

guard の返り値が #f の場合, このcond節をあきらめて,cond節を見つける処理に戻って, 後続するcond節($k+1$番目以降のcond節)の中から真値となるものを見つけに行きます. 一方,guard の返り値が真値(#f 以外の値)だった場合, 今度は test$_k$ の値を実引数として recipient を実行し,その返り値をcond式全体の評価結果として返します. 次の点に注意して下さい.

補足

ガードを使ったcond節がどういった場面で役に立つのかよく分からなかったのですが, 判定式(test$_k$)を2度以上評価したくないときに役立ちそうだということが分かりました.以下にそのような具体例を示します.
;; use-guard.scm
(define (check-ab-native n a b)
  (let loop ((n n))
    (format #t "n=~A\n" n)
    (cond 
     ((= n 1) #t)
     ((= (remainder n a) 0) (loop (/ n a)))
     ((= (remainder n b) 0) (loop (/ n b)))
     (else #f))))

(define (check-ab-guard n a b)
  (let loop ((n n))
    (format #t "n=~A\n" n)
    (cond 
     ((= n 1) #t)
     ((/ n a) (lambda (x) (integer? x))
      => (lambda (x) (loop x)))
     ((/ n b) (lambda (x) (integer? x)) 
      => (lambda (x) (loop x)))
     (else #f))))
上記の2つの手続きは,いずれも,正整数 n と2以上の整数 a,b を受け取って, n が a$^i$b$^j$($i,j \geq 0$)の形をしていたら #t を返し,そうでなければ #f を返します.check-ab-naive はガードを使わずに素直に実現していて,check-ab-guard はガードを使って実現しています.check-ab-naive では,2番目と3番目のcond節は整数除算を2度行っています(余りと商を求めるため).一方,ガードを使ったcond節は整数除算を1度しか行いません.これがガードを使っている理由です.以下に実行例を示します.
guile> (load "use-guard.scm")
      ...... コンパイルメッセージ ......
guile> (check-ab-native 36 3 4)
n=36
n=12
n=4
n=1
$1 = #t
guile> (check-ab-guard 36 3 4)
n=36
n=12
n=4
n=1
$2 = #t
guile> (check-ab-guard 60 3 4)
n=60
n=20
n=5
$3 = #f

具体例

FizzBuzz問題のプログラムを作ります.FizzBuzz問題とは,$k=1,2,...$ に対して次のように動作するプログラムを作成せよ,というプログラミングの問題です. これは,条件分岐に関するお約束の問題です.Wikipedia(上記のリンク)によれば,「コードが書けないプログラマ志願者を見分ける手法をジェフ・アトウッドがFizzBuzz問題 (FizzBuzz Question) として提唱した」とのことです.

以下の fizz-buzz は,整数 $k$ に関する上記の条件に応じて,"FizzBuzz","Fizz","Buzz",または $k$ 自身を返します.fizz-buzz-main は a 以上 b 以下の整数に対してFizzBuzzします.
;; fizzbuzz1.scm
(define (fizz-buzz k)
  (define (divisible-by? m) (= (remainder k m) 0))
  (cond
   ((divisible-by? 15) "FizzBuzz")
   ((divisible-by?  3) "Fizz")
   ((divisible-by?  5) "Buzz")
   (else k)))

(define (fizz-buzz-main a b)
  (let loop ((k a))
    (when (<= k b)
      (display (fizz-buzz k)) (newline)
      (loop (1+ k)))))
guile> (load "fizzbuzz1.scm")
      ...... コンパイルメッセージ ......
guile> (fizz-buzz-main 10 20)
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz

具体例

FizzBuzz問題のプログラムを, 判定式だけからなるcond節を使って無理やり実現してみます.
;; fizzbuzz2.scm
(define (fizz-buzz k)
  (define (divisible-by? m str) 
    (if (= (remainder k m) 0) str #f))
  (cond 
   ((divisible-by? 15 "FizzBuzz"))
   ((divisible-by?  3 "Fizz"))
   ((divisible-by?  5 "Buzz"))
   (k)))

(define (fizz-buzz-main a b)
  (let loop ((k a))
    (when (<= k b)
      (display (fizz-buzz k)) (newline)
      (loop (1+ k)))))
実行例は省略します.

具体例

FizzBuzz問題のプログラムを, 矢印(=>) を含むcond節を使って無理やり実現してみます. 下記の divisible-by? は k が m で割り切れるときには '("FizzBuzz" "Fizz" "Buzz") というリストを返し,そうでないときには #f を返します. 各cond節は,リストが返ってきたとき,car,cadr,caddr を使ってリストから文字列を取り出してcond式の返り値として返しています.
;; fizzbuzz3.scm
(define (fizz-buzz k)
  (define (divisible-by? m) 
    (if (= (remainder k m) 0)
        '("FizzBuzz" "Fizz" "Buzz")
        #f))
  (cond 
   ((divisible-by? 15) => car)
   ((divisible-by?  3) => cadr)
   ((divisible-by?  5) => caddr)
   (else k)))

(define (fizz-buzz-main a b)
  (let loop ((k a))
    (when (<= k b)
      (display (fizz-buzz k)) (newline)
      (loop (1+ k)))))
実行例は省略します.

case 式

case 式はキー式(以下のkey)の結果とデータム(以下のdatum)の比較に基づいて条件分岐(場合分け)を行うための構文です.case 式は次の文法に沿って記述します.
(case key case-clause+) syntax
(case key case-clause* else-clause) syntax
   case-clause  ::=  ((datum*) expression+)
                    |  ((datum*) => recipient)
   else-clause  ::=  (else expression+)
                    |  (else => recipient)
   key          ::= expression
   recipient    ::=  1引数の手続き
   expression   ::=  式
   datum        ::=  what the read procedure successfully parses.
補足:

case 式は,おおまかに見ると次のような形式をしています.

(case key
 (($d_{1,1}$ $d_{1.2}$ ... ) case-clause$_1$)
  ......
 (($d_{n,1}$ $d_{n.2}$ ... ) case-clause$_n$)

または

(case key
 (($d_{1,1}$ $d_{1.2}$ ... ) case-clause$_1$)
  ......
 (else ...)

ここで,$d_{i,j}$は datum を表しています.Guile はこのcase式を次のように処理します.

以下,case 節や else 節のパターンに応じて,それらの評価の仕方を説明します.

((datum*) expression+) または (else expression+) の評価

これらの場合,expression+ の中の式を先頭から順に評価していって,最後の式の結果を case 式の結果として返します.

((datum*) => recipient) または (else => recipient) の評価

これらの場合,keyの値を実引数として recipient を実行し,その返り値を case 式の結果として返します.次の点に注意して下さい.

記録(case 式の動作について)

数,文字,シンボルを datum とする case 式は機能するようですが,文字列やリストを datum とする case 式は機能しないようです. 以下に実行例を示します.
;; Guile 3.0.5 
guile> (case 30 ((10 20) 'first) ((30 40) 'second) (else 'third))
$1 = second
guile> (case 'C ((A B) 'first) ((C D) 'second) (else 'third))
$2 = second
guile> (case #\c ((#\a #\b) 'first) ((#\c #\d) 'second) (else 'third))
$3 = second
guile> (case "c" (("a" "b") 'first) (("c" "d") 'second) (else 'third))
;;; <stdin>:4:12: warning: datum "a" cannot be meaningfully compared using `eqv?' in clause (("a" "b") (quote first)) of case expression (case "c" (("a" "b") (quote first)) (("c" "d") (quote second)) (else (quote third)))
      ...... 同様のwarningメッセージが続きます(省略) ......
$4 = third
guile> (case '(3 4) (((1 2)) 'first) (((3 4)) 'second) (else 'third))
      ...... 上と同様のwarningメッセージが続きます(省略) ......
$5 = third
warningメッセージ(赤字)はナゾです. 素直に解釈するならば,case 式の datum として "a" や '(1 2) を指定しても無意味であると読めます.でも,R7RS[7.1.2. External representations]によれば, 文字列表記やリスト表記も指定できるはずです.

おそらく,文字列やリストに限らず,eqv? の判定に場所(location)が関係する datum は使えないと思われます.もっとも,そういった case 式を使うことはないだろうと思うので,実用上困ることはないでしょう.

記録

datum に関して筆者が把握している実践上の注意点として, シンボルを datum とするとき, クォートを付けてはいけません.R7RS[7.3. Derived expression types] に示されている case 式のマクロ定義によれば,case 式を評価するとき datum のリストは
'(datum ... )
といったようにクォートを付けて処理されます. そのため,シンボルにクォートを付けていると,そのクォートも datum の一部として処理されることになります.

具体例

datum としてシンボルを使った具体例を示します. 上で述べたように,シンボルにクォートを付けてはいけません. 以下の3番目の case 節ではわざとクォートを付けています.
;; case-example.scm
(define (case-test x)
  (case x 
    ((Alice Carol Elena) "Hello")
    ((Bob David) "Hi")
    (('Kajya) "Hey")
    (else "who-are-you")))
上でも述べたように,case 式を評価するとき,3番目の case 節にある ('Kajya) は
'('Kajya)
といったリストに置き換えて処理されると考えることができます.さらに, シングルクォートは qoute 式の糖衣構文であることから,結局 ('Kajya) は
'((quote Kajya))
といった,(quote Kajay) を datum とするリストとして処理されることになります. そのため,上記の手続きをコンパイルしてみると,ナゾのwarningメッセージ(リストを datum にしても無意味といった趣旨のメッセージ)が出てきます. ただし,コンパイルは成功します.
guile> (load "case-example.scm")
      ...... コンパイルメッセージ ...... 
;;;   ...... : warning: datum (quote Kajya) cannot be meaningfully compared using `eqv?' in clause (((quote Kajya)) "Hey") of case expression (case x ((Alice Carol Elena) "Hello") ((Bob David) "Hi") (((quote Kajya)) "Hey") (else (quote who-are-you)))
      ...... コンパイルメッセージ(続) ...... 
guile> (case-test 'Elena)
$1 = "Hello"
guile> (case-test 'David)
$2 = "Hi"
guile> (case-test 'Kajya)
$3 = "who-are-you"
最後の手続き呼び出しは,Kajya を datum とする case 節はないことになるので,else 節を評価しています.

when 式 と unless 式

when 式と unlesse 式は次の文法に沿って記述します.
(when test expression+) syntax
(unless test expression+) syntax
   test        ::= expression
   expression  ::=  式

when 式

Guile[6.11.2 Simple Conditional Evaluation] は,上記の when 式は
(if test (begin expression+))
といった if 式に等価である(とだけ),と述べています. 従って,when 式は,test の結果が真値(#f 以外の値)のとき,expression+ を先頭から順に評価していって, 最後の式の評価結果を返します. test の結果が #f のとき,expression+ を評価せずに終了します.そのときの返り値は unspecified です.

unless 式

Guile[6.11.2 Simple Conditional Evaluation] は,上記の unless 式は
(if (not test) (begin expression+))
といった if 式に等価である(とだけ),と述べています. 従って,unless 式は,test の結果が #f のとき,expression+ を先頭から順に評価していって, 最後の式の評価結果を返します.test の結果が真値(#f 以外の値)のとき,expression+ を評価せずに終了します. そのときの返り値は unspecified です.

記録

R7RS[4.2.1. Conditionals] は,when 式と unless 式の返り値は,test の結果に関わりなく unspecified であると述べています.一方,R7RS[7.3. Derived expression types] に示しているマクロ定義は,上で述べた if 式に翻訳できるとしています. 両者の間に齟齬があります.ちなみに,上で述べた返り値の説明は REPL によって確認した結果に基づいています.

and 式,or 式,and=> 式,他

and 式,or 式,and=> 式は次の文法に沿って記述します.
(and expression*) syntax
(or expression*) syntax
(and=> expression procedure) procedure
   expression  ::=  式
   procedure   ::=  1引数の手続き

and 式

Guile は and 式を次のように評価します. expression を先頭から順に評価していきます. その結果が真値(#f 以外の値)である限り評価を続けます. ある式の結果が #f になったらその時点で直ちに評価を終えて,#f を and 式の結果として返します.一方,最後の式まで #f にならなかったら,最後の式の結果を返します. なお,expression を1つも指定しない (and) については #t を返します.

expression は多値を返す式でもかまわないようです. 多値を返す式を途中に指定したときには,その結果の1番目の値だけが使われるようです. 一方,多値を返す式を最後に指定していて,かつ,その最後の式まで評価が到達したときには,そのすべての値が and 式の結果として返されるようです. これらの事項は REPL を使って確認しています.

or 式

Guile は or 式を次のように評価します. expression を先頭から順に評価していきます. その結果が #f である限り評価を続けます. ある式の結果が真値(#f 以外の値)になったらその時点で直ちに評価を終えて, その真値を or 式の結果として返します. 一方,最後の式まで #f だったら or 式の結果として #f を返します. なお,expression を1つも指定しない (or) については #f を返します.

expression は多値を返す式でもかまわないようです. 多値を返す式を途中に指定したときには,その結果の1番目の値だけが使われるようです. 一方,多値を返す式を最後に指定していて,かつ,その最後の式まで評価が到達したときには,そのすべての値が and 式の結果として返されるようです. これらの事項は REPL を使って確認しています.

and=> 式

and=> は (ice-9 boot-9) モジュールの中で次のような手続きとして定義されています. 下記の value は上記の文法規則における expression のことです.
(define (and=> value procedure)
  (and value (procedure value)))
これより,and=> は,まずvalue(expression)を評価し, その結果が #f だったら #f を返します. 一方,value(expression)の結果が真値(#f 以外の値)たっだら, その真値に procedure を適用した結果を返します. 次の点に注意して下さい. 以上の事項は REPL を使って確認しています. ちなみに,Guileのライブラリモジュールを覗いてみると,and=> はあちらこちらで利用されています.利用頻度が高めの手続きのようです.

具体例

fizzbuzz3.scm で示したプログラムは and,or,and=> を使って次のように作り変えることができます.
;; and-or.scm
(define (fizz-buzz k)
  (define (divisible-by? m) 
    (and (= (remainder k m) 0)
         '("FizzBuzz" "Fizz" "Buzz")))
  (or
   (and=> (divisible-by? 15) car)
   (and=> (divisible-by?  3) cadr)
   (and=> (divisible-by?  5) caddr)
   k))

(define (fizz-buzz-main a b)
  (let loop ((k a))
    (when (<= k b)
      (display (fizz-buzz k)) (newline)
      (loop (1+ k)))))
guile> (load "and-or.scm")
      ...... コンパイルメッセージ ......
guile> (fizz-buzz-main 10 20)
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
and や or を使うとプログラムがすっきりして見える気がします(効率的にどうなのかは分かりませんが).ただ,他言語(例えば,C言語)から来た人は最初は戸惑うだろうと思います.いまは慣れましたが,筆者も最初は戸惑いました.

and-let* 式

and 式と let* 式を組み合わせた and-let* 式 というのもあります. 詳しくはリンク先を参照して下さい.
(おしまい)