Guile基礎/拡張版ラムダ式(case-lambda と case-lambda*)

変更履歴

概 要

目 次

参考資料

case-lambda式

lmabda式は次の文法に沿って記述します.
(case-lambda [docstring] cl-clause*) syntax
   docstring   ::= 文字列 
   cl-clause   ::= (formals body)
   formals     ::=  (identifier*)
                   |  identifier
                   |  (identifier+ . identifier)
   identifier  ::=  識別子
   body        ::=  definition* expression+
   definition  ::=  定義(define形式など)
   expression  ::=  式
補足: Gule 3.0 以降,cl-clauseの本体は定義と式を交互に記述してもよいことになっています.つまり,以下に示すように,cl-clauseの本体はbodyを複数指定してよいことになります.
   cl-clause   ::= (formals body+)

参照: Guile[6.10.3 Internal definitions] の最後に次の一文があります.
Relatedly, it used to be that internal definitions had to precede all expressions in the body; this restriction was relaxed in Guile 3.0.

case-lambda は case-lambda 式の構文キーワードです.docstring は処理内容などのドキュメントを記述した文字列です.不要ならば省略できます.角括弧は省略可能であることを示しています(case-lambda 式の構文要素ではありません).formalsbody は lambda 式とまったく同じものです. 従って,cl-clause は lambda 式の仮引数と本体を記述したもので,case-lambda 式は仮引数と本体の組み合わせを並べたものです.cl-clause のことを cl-節(より正確には case-lambda 節)と呼ぶことにします.

case-lambda式の評価

case-lambda 式は, 実引数に対する複数通りのパターンに対処する手続きを作るためのものです. lambda 式の場合と同様に,case-lambda 式を評価したときの環境が組み合わされるので, 複数通りの実引数パターンに対処するクロージャを作ります. そのクロージャが case-lambda 式の評価結果になります. lambda 式と同様に,本体(body)は手続きが実行されるまで評価されません.

手続き呼び出し

case-lambda 式によって作られた手続きを呼び出したとき,Guileは,先頭のcl-節から順に,仮引数(formals)と実引数を比較していって, 実引数を受け取れる仮引数(formals)を持った最初のcl-節(cl-clause)の本体(body)を実行します. その実行の仕方は lambda 式とまったく同じです. 従って,その本体(body)の最後に評価した式の値を手続き呼び出しの結果(返り値)として返します.従ってまた,他の本体(body)を評価することはありません.実引数を受け取れる仮引数(formals)がなかったときにはエラーが発生します.

以下では, ある仮引数(formals)が実引数を受け取れることを, その仮引数(formals)は実引数にマッチすると言ったり, その仮引数を含むcl-節が実引数にマッチすると言ったりすることにします.

具体例

仮引数と実引数のマッチを確認することだけを目的として, 次のような手続き定義します. 下記のcl-節を上から順に cl-clause-1 〜 cl-clause-5 と呼ぶことにします.
;; case-lam.scm
(define test-cl 
  (case-lambda 
    (() (display "cl-clause-1\n"))
    ((a) (display "cl-clause-2\n"))
    ((a b) (display "cl-clause-3\n"))
    ((a b . c) (display "cl-clause-4\n"))
    ((a b x y) (display "cl-clause-5\n"))))
実引数を1つずつ増やしながら test-cl を実行してみます.
guile> (load "case-lam.scm")
      ...... コンパイルメッセージ ......
guile> (test-cl)
cl-clause-1
guile> (test-cl 1)
cl-clause-2
guile> (test-cl 1 2)
cl-clause-3
guile> (test-cl 1 2 3)
cl-clause-4
guile> (test-cl 1 2 3 4)
cl-clause-4
1番目の無引数の手続き呼び出しは cl-clause-1 にマッチするので,cl-clause-1 の本体が実行されます.2番目は,cl-clause-1 にはマッチせず,cl-clause-2 にマッチするので,cl-clause-2 の本体が実行されます.3番目は cl-clause-3 の本体が実行されます.

4番目と5番目は,ともに cl-clause-1 〜 clause-3 にはマッチせず,cl-clause-4 にマッチするので,cl-clause-4 の本体が実行されます.可変的な仮引数(上記のc)は実引数を幾つでも受け取ることができるので,3個以上の実引数を指定して test-cl を呼び出したときには cl-clause-4 の本体が実行されます. 従って,cl-clause-5 が選ばれることはありません.

可変的な仮引数を指定したcl-節のうしろに記述したcl-節は, 構文的に間違いではありませんが,ムダです. つまり,可変的な仮引数を含む cl-節 は case-lambda 式の最後に記述するものです.

具体例

以下の disp-func 手続きは,$m$引数の数値関数 func(注:$m \geq 0$) , 数値の個数 num,初期値と増分からなる$m$個のペア (init$_1$ . diff$_1$),(init$_2$ . diff$_2$),...,(init$_m$ . diff$_m$) に対して,
func(init$_1$, ..., init$_m$),
func(init$_1$+diff$_1$, ..., init$_m$+diff$_m$),
func(init$_1$+2*diff$_1$, ..., init$_m$+2*diff$_m$),
   ......
func(init+(num-1)*diff, ..., init$_m$+(num-1)*diff$_m$)
の値を適当な形式で表示します.$m=0$〜$2$ の場合はcl-節を分けて個別的に処理し,$m \geq 3$ の場合は apply を使って統一的に処理しています.add-comma は,関数値を表示するときにfunc の引数の間にカンマ(,)を挿入するための補助的な手続きです.
;; disp-func.scm
(define disp-func 
  (case-lambda
    ((func num)
     (let loop ((k 0))
       (when (< k num)
         (format #t "func()=~A\n" (func))
         (loop (1+ k)))))
    ((func num init-diff-1)
     (let ((init (car init-diff-1)) (diff (cdr init-diff-1)))
       (let loop ((k 0) (x init))
         (when (< k num)
           (format #t "func(~A)=~A\n" x (func x))
           (loop (1+ k) (+ x diff))))))
    ((func num init-diff-1 init-diff-2)
     (let ((init-1 (car init-diff-1)) (diff-1 (cdr init-diff-1))
           (init-2 (car init-diff-2)) (diff-2 (cdr init-diff-2)))
       (let loop ((k 0) (x init-1) (y init-2))
         (when (< k num)
           (format #t "func(~A,~A)=~A\n" x y (func x y))
           (loop (1+ k) (+ x diff-1) (+ y diff-2))))))
    ((func num . init-diffs)
     (let((inits (map car init-diffs))
          (diffs (map cdr init-diffs)))
       (let loop ((k 0) (xs inits))
         (when (< k num)
           (format #t "func(~A)=~A\n" (add-comma xs) (apply func xs))
           (loop (1+ k) (map + xs diffs))))))
    ))

(define (add-comma xs)
  (define (x-with-comma x) (string-append "," (number->string x)))
  (substring (string-join (map x-with-comma xs)) 1))
以下,適当に実行しています.
guile> (load "disp-func.scm")
      ...... コンパイルメッセージ ......
guile> (disp-func (lambda () 10) 5)
func()=10
func()=10
func()=10
func()=10
func()=10
guile> (disp-func (lambda (x) (* x x)) 5 '(0 . 1))
func(0)=0
func(1)=1
func(2)=4
func(3)=9
func(4)=16
guile> (disp-func (lambda (x y) (+ x y)) 5 '(0 . 1) '(1 . 2))
func(0,1)=1
func(1,3)=4
func(2,5)=7
func(3,7)=10
func(4,9)=13
guile> (disp-func (lambda (x y z) (+ x y z)) 5 '(0 . 1) '(2 . 1) '(3 . 1))
func(0 ,2 ,3)=5
func(1 ,3 ,4)=8
func(2 ,4 ,5)=11
func(3 ,5 ,6)=14
func(4 ,6 ,7)=17

記録

各cl-節の処理内容が比較的容易に一本化できるのであれば,case-lambda 式(や case-lambda* 式)よりも lambda* 式を使ったほうがプログラムが簡潔になります. 以下にそういった具体例を示します.

以下の disp-values 手続きは,1引数の数値関数 func ,数値の個数 num,初期値 init,増分 diff に対して,
func(init),func(init+diff),func(init+2*diff),...,func(init+(num-1)*diff)
の値を適当な形式で表示します. ただし,数値関数 func は関数名を表す文字列(fname)で指定し, 初期値や増分は省略できるようにしています. 初期値を省略したときの既定値は 0.0 にし, 増分を省略したときの既定値は 1.0 にしています. disp-sub は関数値を表示するための補助手続きです.
(define disp-values
  (case-lambda 
    ((fname num) 
     (disp-sub fname num 0.0 1.0))
    ((fname num init) 
     (disp-sub fname num init 1.0))
    ((fname num init diff) 
     (disp-sub fname num init diff))))

(define (disp-sub fname num init diff)
  (let ((func (eval-string fname)))
    (let loop ((k 0) (x init))
      (when (< k num)
        (format #t "~A(~A) = ~A\n" fname x (func x))
        (loop (1+ k) (+ x diff))))))
この例の場合,各cl-節がまったく同じ処理を行うので, その処理を補助的な手続き(disp-sub)にまとめています. そうならば,以下のように lambda* 式を使ったほうがプログラムが簡潔になります.
(define* (disp-values fname num #:optional (init 0.0) (diff 1.0))
  (let ((func (eval-string fname)))
    (let loop ((k 0) (x init))
      (when (< k num)
        (format #t "~A(~A) = ~A\n" fname x (func x))
        (loop (1+ k) (+ x diff))))))
一般に,case-lambda,case-lambda*,lambda* は比較検討しながら使ったほうがよいと思います.

記録

Guileの map や for-each は,lambda* ではなく case-lambda を使って定義されています.apply を使えば lambda* 式によっても簡潔に実装できます.実際,apply を単純に使って実装するのならば,可変的仮引数の lambda 式を使えば十分でしょう. しかし,apply は一般に実引数のコピーを作るので効率面でデメリットがあります. そのため,case-lambda 式を使うことによって,発生頻度がとても高いと思われる「少ない引数の場合」に apply の適用を避けているのだと思います.

一方,map や for-each を apply の適用をなるべく避けつつ lambda* 式を使って実装することを検討してみると,引数の個数を判定する処理が必要になります. でも,それって case-lambda 式のcl-節が行っていることです.

map や for-each は case-lambda 式を使った雛形と言えます. 分割統治法に対するクイックソートやマージソートみたいなものです. ちなみに,上で示した具体例はこれらの定義を参考に考えたものです.

記録

コアシステムのソースコードを読み込めるほどの力量は筆者にはありませんが. 漠然と眺めていると,各cl-節を内部的なデータ構造に変換したものを連結リストにしているように感じます.仮引数と実引数のマッチング処理をどのタイミングで行っているのかを知りたいと思ったのですが,たぶん,すべての呼び出しに対してコンパイル時ではなく実行時に行っているのだろうと思います. 従って,「case-lambda式の評価」で述べたことはウソではないと思われます.

case-lambda*式

lmabda* 式は次の文法に沿って記述します.
(case-lambda* [docstring] cl-star-clause*) library syntax
   cl-star-clause ::=  (formals* body)
   formals*       ::=  ([required] [optional] [keyword] [rest]) 
   required     ::=  identifier+
   optional       ::=  #:optional param+
   keyword        ::=  #:key param* [#:allow-other-keys]
   param          ::=  identifier
                      |  (identifier expression)
   rest           ::=  #:rest identifier
                      |  . identifier
   identifier     ::=  識別子
   body           ::=  definition* expression+
   definition     ::=  定義(define形式など)
   expression     ::=  式
補足: Gule 3.0 以降,cl-star-clauseの本体は定義と式を交互に記述してもよいことになっています.つまり,以下に示すように,cl-star-clauseの本体はbodyを複数指定してよいことになります.
   cl-star-clause   ::= (formals* body+)

参照: Guile[6.10.3 Internal definitions] の最後に次の一文があります.
Relatedly, it used to be that internal definitions had to precede all expressions in the body; this restriction was relaxed in Guile 3.0.

case-lambda* 式は case-lambda の lambda* バージョンです. 上記の formals*body は lambda* 式のものと全く同じです(注:body は lambda 式と共通です). 従って,cl-star-clause は lambda* 式の仮引数と本体を記述したもので,case-lambda* 式はその仮引数と本体の組み合わせを並べたものです. 以下,cl-star-clause のことを cl*-節 と呼ぶことにします.

case-lambda*式の評価

case-lambda 式と同様に,case-lambda* 式は複数の実引数のパターンに対処するクロージャを作ります.そのクロージャが case-lambda* 式の評価結果です.lambda* 式(や lambda 式)と同様に,手続きが実行されるまで本体(body)は評価されません.

手続き呼び出し

case-lambda* 式によって作られた手続きを呼び出したとき,case-lambda 式の場合と同様に,Guile は実引数にマッチするcl*-節を探します. ただ,case-lambda 式の場合との大きな違いは, マッチしても仮引数を束縛できない場合があることです.つまり, マッチ条件は束縛条件の必要条件ですが十分条件にはなっていません. 以下では,まずマッチ条件を説明し,そのあとで仮引数の束縛について説明します.

以下の説明では次を仮定します. このとき,実引数 $v_1$ ... $v_{\ell}$ が $C$ にマッチするとは,次の 1. と 2. が成り立つときを言います.
  1. $\ell \geq n$.
  2. $\ell > n$ のとき,$v_{n+1}$ ... $v_{\ell}$ は次のいずれかの条件を満たす.
    1. $C$ がキーワード仮引数を持たず,かつ,$v_{n+1}$ ... $v_{\ell}$ は(キーも含めて)もれなくオプション仮引数によって捕捉される.
    2. $C$ がキーワード仮引数を持っていて,かつ,$v_{n+1}$ ... $v_{\ell}$ がキーを含まず,かつ,$v_{n+1}$ ... $v_{\ell}$ はもれなくオプション仮引数によって捕捉される.
    3. $C$ がキーワード仮引数を持っていて,かつ,$v_{n+1}$ ... $v_{\ell}$ がキーを含んでいて,かつ,先頭から見て最初のキーを $v_{k+1}$ とおくとき $k+1 = n+1$(すなわち,$k=n$)かまたは ($k>n$ならば)$v_{n+1}$ ... $v_k$ はもれなくオプション仮引数によって捕捉される.

Guileは,先頭のcl*-節から順に実引数にマッチするか否かを検査していって, 最初にマッチしたcl*-節を選択します.そのあと,仮引数を次のように束縛します. 以下,上と同様に,必須仮引数の個数を $n$ とおき,実引数を $v_1$ ... $v_{\ell}$ とします.
  1. まず必須仮引数を先頭から$n$個の実引数 $v_1$ ... $v_n$ に束縛します. ここで,$v_1$〜$v_n$の中にキーが含まれていたとしても, それを必須仮引数を束縛するための値の1つとして処理します. 言い換えると,この束縛ではキーワード仮引数(やオプション仮引数やレスト仮引数)を無視します.
  2. 次に $\ell > n$ のとき,$v_{n+1}$〜$v_{\ell}$ を次のように処理します.
    1. 上記の 2.a. が成り立つとき,$v_{n+1}$〜$v_{\ell}$ はオプション仮引数を束縛します.
    2. 上記の 2.b. が成り立つとき,$v_{n+1}$〜$v_{\ell}$ はオプション仮引数を束縛します.
    3. 上記の 2.c. が成り立つとき,$v_{n+1}$〜$v_{k}$ はオプション仮引数を束縛し,$v_{k+1}$〜$v_{\ell}$におけるキー/実引数はキーワード仮引数を束縛し,その他の残りの実引数はレスト仮引数を束縛します.
以上の束縛のもとでcl*-節の本体(body)を実行し, 最後に評価した式の結果を返します.

上記の 2.c. が成り立つ場合,マッチしても束縛できない可能性があります.Guileは,マッチ条件において $v_{k+1}$〜$v_{\ell}$ がキーワード仮引数やレスト仮引数によって捕捉できるか否かを検査しません.Guileのマニュアルは,効率の点からその検査を行っていないと述べています.

具体例

以下は上記の上で述べたことを確認するためだけの実験的なプログラムです.
;; case-lam.scm
(define test-cl*
  (case-lambda*
   ((x #:optional ox) 
    (format #t "clause-1: x=~A ox=~A\n" x ox))
   ((x #:optional ox oy) 
    (format #t "clause-2: x=~A ox=~A oy=~A\n" x ox oy))
   ((x y #:optional ox #:key kx) 
    (format #t "clause-3: x=~A y=~A ox=~A kx=~A\n" x y ox kx))
   ((#:key kx #:rest z) 
    (format #t "clause-4: kx=~A z=~A\n" kx z))))
プログラムをロードしたあと, 適当な実引数に対して test-cl* 手続きを呼び出してみます.
guile> (load "case-lam.scm")
      ...... コンパイルメッセージ ......
まず 2.a. が成り立つ場合を試してみます. 以下の $1 = #t はtest-cl* の返り値(formatの返り値)です.
guile> (test-cl* 'A 'B)
clause-1: x=A ox=B
$1 = #t
次に 2.b. が成り立つ場合を試してみます.以下の実引数に対して,clause-1 は仮引数の個数が不足していてマッチしません.一方,clause-2 は 2.b. のマッチ条件を満たします. そのため clause-2 が選択され,実引数は(キー #:kx も含めて)必須仮引数 x とオプション仮引数 ox,oy を束縛します.
guile> (test-cl* 'A #:kx 'X)
clause-2: x=A ox=#:kx oy=X
$2 = #t
次の 2.c. が成り立つ場合を試してみます.以下の実引数に対して,clause-1 と clause-2 は仮引数の個数が不足していてマッチすることはありません.一方,clause-3 は上記の条件 2.c. を満たします.従って,clause-3 が選択され,'A と 'B は必須仮引数(x,y)を束縛し,#:kx 'X はキーワード仮引数 kx を束縛します.
guile> (test-cl* 'A 'B #:kx 'X)
clause-3: x=A y=B ox=#f kx=X
$3 = #t
以下の実引数に対して,clause-1 〜 clause-3 は必須仮引数とオプション仮引数の個数が不足していてマッチすることはありません.一方,clasue-4 は上記の条件 1. と 2.c. を 満たします.clause-4 は必須仮引数がないので(つまり,$n=0$なので)条件 1. は明らかに満たします.さらに,条件 2.c. における $k$ も $k=0$ なので,条件 2.c. を満たします.従って,以下の実引数に対して clause-4 が選択され,#kx: 'X はキーワード仮引数 kx を束縛し,実引数戦隊からなるリストはレスト仮引数 z を束縛します.
guile> (test-cl* 'A 'B 'C 'D #:kx 'X)
clause-4: kx=X z=(A B C D #:kx X)
$5 = #t

最後にマッチするけれども束縛できない実行例を示します. 以下の実引数に対して,clause-1 と clause-2 は仮引数の個数が不足していてマッチすることはありません. 一方,clause-3 は上記の条件 2.c. を満たします. 従って,clause-3 が選択され,束縛処理に移行します. しかし,'Y を捕捉する仮引数がなくてエラーが発生します. 以下の赤字がエラーメッセージです.
guile> (test-cl* 'A 'B #:kx 'X 'Y)
ice-9/boot-9.scm:1669:16: In procedure raise-exception:
Invalid keyword: Y
(余談) clause-3 はレスト仮引数がないので, Guile は #:kx 以降の実引数はキーと実引数が交互に並んでいるものと仮定して束縛処理を行います.そのため 'Y のところにはキーが指定されていなければならないのですが, ヘンなシンボル('Y)が指定されているので上記のエラーメッセージになっているのです.

Guileは,マッチング処理の条件 2.c. のところでキーワード仮引数とレスト仮引数に関する束縛検査を端折っています.しかし,それを真面目に検査していれば,上の実行例は clause-4 を選ぶことになって無事実行されたはずです.そこで,この点を確認するために,clause-3 を除去した次の手続きを同じ実引数に対して実行してみます.
(define test-cl*2
  (case-lambda*
   ((x #:optional ox) 
    (format #t "clause-1: x=~A ox=~A\n" x ox))
   ((x #:optional ox oy) 
    (format #t "clause-2: x=~A ox=~A oy=~A\n" x ox oy))
   ((#:key kx #:rest z) 
    (format #t "clause-4: kx=~A z=~A\n" kx z))))
こちらは clause-4 がマッチして,'Y はレスト仮引数に捕捉されて,無事実行されます.
guile> (test-cl*2 'A 'B #:kx 'X 'Y)
clause-4: kx=X z=(A B #:kx X Y)
$6 = #t
(おしまい)