[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9. 制御構造

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Control%20Structures"
"elisp/制御構造"へのコメント(無し)

Lispプログラムは、式、すなわち、フォーム(forms、see 節 8.2 フォームの種類)から 成ります。 フォームを制御構造(control structures)で囲むことで、 フォームの実行順序を制御します。 制御構造はスペシャルフォームであり、 その内側にあるフォームの実行をいつ行うか、行わないか、 何回行うかを制御します。

もっとも単純な実行順序は逐次実行です。 最初のフォームaを実行し、 それからつぎのフォームbを実行し、といった具合です。 関数の本体やLispコードのファイルのトップレベルに複数のフォームを順に書くと、 このようになります。 つまり、書かれている順番にフォームを実行します。 これをテキスト上の順序(textual order)と呼びます。 たとえば、関数本体が2つのフォームabから成る場合、 関数を評価すると、まずaを評価し、つぎにbを評価して、 関数の値はbの値になります。

明示的な制御構造により、逐次実行以外の実行順序が可能になります。

Emacs Lispには数種類の制御構造があり、 逐次実行の変形、条件付き実行、繰り返し実行、 (制御された)ジャンプなどです。 これらすべては、以下に説明します。 組み込みの制御構造はスペシャルフォームです。 というのは、それらのサブフォームは必ずしも評価しませんし、 逐次評価するわけでもないからです。 マクロを使えば、独自の制御構造の構文を定義できます(see 節 12. マクロ)。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.1 逐次実行

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Sequencing"
"elisp/逐次実行"へのコメント(無し)

現れる順番にフォームを評価することは、 1つのフォームから別のフォームへ制御を移すもっとも一般的な方法です。 関数本体などのある種の文脈では、自動的にこのようになります。 それ以外では、これを行う制御構造の構文を使う必要があります。 prognがその制御構造で、Lispのもっとも単純な制御構造です。

スペシャルフォームprognはつぎのような形です。

 
(progn a b c ...)

これは、フォーム、abc、…をこの順に評価します。 これらのフォームをprognフォームの本体と呼びます。 本体の最後のフォームの値が、progn全体の値になります。

初期のころのLispでは、prognは、 2つ以上のフォームを逐次実行しそれらの最後の値を使う唯一の方法でした。 しかし、プログラマは、(当時は)1つのフォームしか許されていない 関数の本体では、 prognを使う必要がしばしばあることに気づきました。 そのため、関数本体を『暗黙のprogn』にしたのです。 つまり、実際のprognの本体のように、 複数のフォームを許すようにしたのです。 多くの他の制御構造も、同様に、暗黙のprognです。 その結果、prognは、かつてほどは多用されません。 現在では、unwind-protectandorの内側や、 ifthen部分で必要とされるのがほとんどです。

Special Form: progn forms...
このスペシャルフォームは、formsのフォームすべてを テキスト上の順に評価し、最後のフォームの結果を返す。

 
(progn (print "The first form")
       (print "The second form")
       (print "The third form"))
     -| "The first form"
     -| "The second form"
     -| "The third form"
=> "The third form"

他の2つの制御構造も同様にフォームを逐次評価しますが、 返す値が異なります。

Special Form: prog1 form1 forms...
このスペシャルフォームは、form1formsのフォームすべてを テキスト上の順に評価し、form1の結果を返す。

 
(prog1 (print "The first form")
       (print "The second form")
       (print "The third form"))
     -| "The first form"
     -| "The second form"
     -| "The third form"
=> "The first form"

変数のリストから先頭要素を取り除き、取り除いた要素を返すにはつぎのように書く。

 
(prog1 (car x) (setq x (cdr x)))

Special Form: prog2 form1 form2 forms...
このスペシャルフォームは、form1form2formsの フォームすべてをテキスト上の順に評価し、form2の結果を返す。

 
(prog2 (print "The first form")
       (print "The second form")
       (print "The third form"))
     -| "The first form"
     -| "The second form"
     -| "The third form"
=> "The second form"



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.2 条件付き実行

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Conditionals"
"elisp/条件付き実行"へのコメント(無し)

条件付き制御構造は、選択肢を選びます。 Emacs Lispには、4つの条件付きフォームがあります。 他の言語のものとほとんど同じififの変形であるwhenunless、 一般化したcase文であるcondです。

Special Form: if condition then-form else-forms...
ifは、conditionをもとにして、 then-formelse-formsを選ぶ。 conditionnil以外に評価されると、 then-formを評価し、その結果を返す。 さもなければ、else-formsをテキスト上の順に評価し、 その最後のものの値を返す。 (ifelse部分は、暗黙のprognの例である。 see 節 9.1 逐次実行。)

conditionが値nilであり、かつ、else-formsがないと、 ifnilを返す。

ifがスペシャルフォームであるのは、 選択しなかった分岐をけっして評価しないからである。 したがって、つぎの例では、 printはけっして呼ばれないためtrueは表示されない。

 
(if nil 
    (print 'true) 
  'very-false)
=> very-false

Macro: when condition then-forms...
これはifの変形であり、else-formsがなく、 then-formsは複数のフォームでもよい。 特に、

 
(when condition a b c)

は、つぎとまったく等価である。

 
(if condition (progn a b c) nil)

Macro: unless condition forms...
これはthen-formがないifの変形である。

 
(unless condition a b c)

は、つぎとまったく等価である。

 
(if condition nil
   a b c)

Special Form: cond clause...
condは任意個数の選択肢から1つを選ぶ。 condの各節clauseはリストである必要がある。 このリストのCARがcondition(条件)である。 残りの要素は、あれば、body-forms(本体フォーム)である。 つまり、各節はつぎのようになる。

 
(condition body-forms...)

condは、各節のconditionを評価して、 各節をテキスト上の順に試す。 conditionの値がnil以外であれば、 その節は『成功』する。 そうすると、condはその節のbody-formsを評価し、 body-formsの最後の値がcondの値となる。 残りの節は無視する。

conditionの値がnilであると、 その節は『失敗』し、 condはつぎの節へ移りそのconditionを試す。

conditionnilに評価されると、 すべての節が失敗し、condnilを返す。

clauseは、つぎの形式でもよい。

 
(condition)

この場合、conditionnil以外であると、 conditioncondフォームの値になる。

以下の例には4つの節があり、 xの値が、数、文字列、バッファ、シンボルかどうか調べる。

 
(cond ((numberp x) x)
      ((stringp x) x)
      ((bufferp x)
       (setq temporary-hack x) ; 1つの節に
       (buffer-name x))        ; 複数個の本体フォーム
      ((symbolp x) (symbol-value x)))

最後の節を除くそれよりまえの節がどれも成功しないときには、 最後の節を実行したいことがしばしばある。 これを行うには、(t body-forms)のように 最後の節のconditiontを使う。 フォームttと評価され、けっしてnilではない。 そのため、condがこの節に達したときには、 この節が失敗することはない。

たとえば、つぎのとおり。

 
(cond ((eq a 'hack) 'foo)
      (t "default"))
=> "default"

この式は、aの値がhackのときにはfooを返し、 さもなければ文字列"default"を返すcondである。

任意の条件付き構造は、condifで表現できます。 したがって、どちらを使うかは好みの問題です。 たとえば、つぎのとおりです。

 
(if a b c)
==
(cond (a b) (t c))



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.3 条件の組み合わせ

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Combining%20Conditions"
"elisp/条件の組み合わせ"へのコメント(無し)

本節では、ifcondとともに用いて複雑な条件を表現するために しばしば使われる3つの構造を説明します。 andorの構造は、 複数の条件付き構造の一種として単独で使うこともできます。

Function: not condition
この関数は、conditionが偽であるかどうか調べる。 conditionnilであればtを返し、 さもなければnilを返す。 関数notnullと同一であるが、 空リストかどうか調べる場合には、nullを使うことを勧める。

Special Form: and conditions...
スペシャルフォームandは、 すべてのconditionsが真であるかどうか調べる。 conditionsを1つ1つ書かれた順に評価して調べる。

conditionsのどれかがnilに評価されると、 andの結果は、残りのconditionsに関係なく、nilになる。 つまり、andはただちに完了し、 conditionsの残りを無視する。

conditionsすべてがnil以外であることがわかると、 それらの最後の値がフォームandの値となる。

例を示そう。 最初の条件は整数1を返し、これはnilではない。 同様に、2番目の条件は整数2を返し、nilではない。 3番目の条件はnilなので、残りの条件を評価しない。

 
(and (print 1) (print 2) nil (print 3))
     -| 1
     -| 2
=> nil

andを使ったより現実的な例はつぎのとおり。

 
(if (and (consp foo) (eq (car foo) 'x))
    (message "foo is a list starting with x"))

(consp foo)nilを返すと(car foo)は実行されず、 そのためエラーを回避することに注意。

andは、ifcondで表現できる。 たとえば、つぎのとおり。

 
(and arg1 arg2 arg3)
==
(if arg1 (if arg2 arg3))
==
(cond (arg1 (cond (arg2 arg3))))

Special Form: or conditions...
スペシャルフォームorは、 conditionsの少なくとも1つが真であるかどうか調べる。 conditionsを1つ1つ書かれた順に評価して調べる。

conditionsのどれかがnil以外に評価されると、 orの結果はnil以外になる。 そして、orはただちに完了し、 conditionsの残りを無視する。 戻り値は、nil以外に評価された値である。

conditionsすべてがnilであることがわかると、 ornilを返す。

たとえば、つぎの式は、xが0かnilであることを調べる。

 
(or (eq x nil) (eq x 0))

and構造と同様に、orcondで書き表せる。 たとえば、つぎのとおり。

 
(or arg1 arg2 arg3)
==
(cond (arg1)
      (arg2)
      (arg3))

orifで書くこともだいたいできるが、 途中で抜け出せない。

 
(if arg1 arg1
  (if arg2 arg2 
    arg3))

これは完全には同一ではない。 というのは、arg1arg2を2度評価するからである。 一方、(or arg1 arg2 arg3)は、 どの引数も一度だけ評価する。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.4 繰り返し

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Iteration"
"elisp/繰り返し"へのコメント(無し)

繰り返しとは、プログラムのある部分を何度も実行することです。 たとえば、リストの各要素や0からnの各整数について 1回ずつある計算を行いたい場合です。 Emacs Lispでこれを行うには、スペシャルフォームwhileを使います。

Special Form: while condition forms...
whileは、まずconditionを評価する。 結果がnil以外であれば、formsをテキスト上の順で評価する。 そして、conditionを評価し直し、その結果がnil以外であれば、 再度formsを評価する。 この処理をconditionnilに評価されるまで繰り返す。

繰り返し回数に制限はない。 ループは、conditionnilに評価される、 エラーが発生する、throwによりループから抜け出す (see 節 9.5 非ローカル脱出)のいずれかが起こるまで繰り返される。

フォームwhileの値はつねにnilである。

 
(setq num 0)
     => 0
(while (< num 4)
  (princ (format "Iteration %d." num))
  (setq num (1+ num)))
     -| Iteration 0.
     -| Iteration 1.
     -| Iteration 2.
     -| Iteration 3.
     => nil

終了検査のまえに各繰り返しごとに実行したいことがあれば、 以下のように、それらと終了検査をprognでまとめたものを whileの第1引数にする。

 
(while (progn
         (forward-line 1)
         (not (looking-at "^$"))))

これは、1行先へ移動し、空行に達するまで、移動を繰り返す。 このwhileには本体がなく、 終了検査(かつポイントを実際に動かす)だけであるという点で、 風変わりである。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5 非ローカル脱出

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Nonlocal%20Exits"
"elisp/非ローカル脱出"へのコメント(無し)

非ローカル脱出(nonlocal exit)とは、 プログラムのある場所から別の離れた場所へ制御を移すことです。 Emacs Lispでは、エラーの結果として非ローカル脱出が発生します。 非ローカル脱出は、明示的な制御にも使えます。 非ローカル脱出は、脱出対象の構造で作成したすべての変数束縛を解きます。

9.5.1 明示的な非ローカル脱出: catchthrow    Nonlocal exits for the program's own purposes.
9.5.2 catchthrowの例    Showing how such nonlocal exits can be written.
9.5.3 エラー    How errors are signaled and handled.
9.5.4 非ローカル脱出時の後始末    Arranging to run a cleanup form if an error happens.



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.1 明示的な非ローカル脱出: catchthrow

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Catch%20and%20Throw"
"elisp/明示的な非ローカル脱出:catchthrow"へのコメント(無し)

ほとんどの制御構造は、その構造内での制御の流れだけに影響します。 関数throwは、通常のプログラム実行のこのような規則の例外です。 つまり、要求に従って非ローカルな脱出を行います。 (ほかにも例外はあるが、それらはエラー処理のためだけである。) throwcatchの内側で使い、 そのcatchへ戻ります。

 
(defun foo-outer ()
  (catch 'foo
    (foo-inner)))

(defun foo-inner ()
  ...
  (if x
      (throw 'foo t))
  ...)

フォームthrowを実行すると、対応するcatchへ制御が戻り、 そのcatchはただちに終了します。 throwに続くコードは実行されません。 throwの第2引数は、catchの戻り値として使われます。

関数throwは、その第1引数に基づいて対応するcatchを探します。 つまり、catchの第1引数が throwに指定されたものにeqであるcatchを探します。 そのようなcatchが複数個ある場合には、 もっとも内側のものを優先します。 したがって、上の例では、throwfooを指定し、 foo-outercatchは同じシンボルを指定しているので、 そのcatchを使います (ただし、これらのあいだには他の一致するcatchがないとして)。

throwの実行により、 対応するcatchまでのすべてのLispの構造を抜け出します。 これには関数呼び出しも含みます。 letや関数呼び出しなどの束縛を作る構造からもこのように抜け出すので、 通常どおり抜け出す場合と同様に束縛を解きます (see 節 10.3 ローカル変数)。 同様に、throwは、save-excursion(see 節 29.3 エクスカージョン)で 保存したバッファや位置情報、 save-restrictionで保存したナロイング状態、 save-window-excursion(see 節 27.16 ウィンドウ構成)で保存した ウィンドウの選択状態も復元します。 さらに、スペシャルフォームunwind-protectで設定した後始末を このフォームから抜け出すときに実行します(see 節 9.5.4 非ローカル脱出時の後始末)。

throwは、テキスト上で、 ジャンプ先であるcatchの内側に現れる必要はありません。 throwは、catch内から呼ばれた別の関数からも戻ることもできます。 throwの実行が、 時間的にcatchに入ったあとで、かつ、それから抜けるまえである限り、 throwは対応するcatchを参照できます。 エディタコマンドループ(see 節 20.11 再帰編集)から抜ける exit-recursive-editなどのコマンドで throwを使えるのは、このような理由からです。

Common Lispに関した注意: Common Lispを含むほとんどの他のLispには、 非逐次的に制御を移す方法がいくつかある。 たとえば、returnreturn-fromgo。 Emacs Lispにはthrowしかない。

Special Form: catch tag body...
catchは、関数throw向けに戻り位置を確立する。 その戻り位置は、tagによって他の戻り位置と区別される。 tagは、nil以外ならば任意のLispオブジェクトでよい。 引数tagは、戻り位置を確立するまえに、通常どおり評価される。

戻り位置を確立してから、catchは、bodyのフォームを テキスト上の順に評価する。 エラーや非ローカル脱出なしにフォームの実行が普通に終了した場合、 catchは、最後の本体フォームの値を返す。

bodyの内側で、tagと同じ値を指定したthrowが実行されると、 catchはただちに終了する。 このとき返す値は、throwの第2引数に指定されたものである。

Function: throw tag value
throwの目的は、 catchでまえもって確立しておいた戻り位置へ復帰することである。 引数tagは、さまざまな既存の戻り位置から選ぶために使う。 tagは、catchで指定した値とeqである必要がある。 tagに複数の戻り位置が一致する場合には、もっとも内側のものを使う。

引数valueは、対応するcatchの戻り値として使う。

タグtagである有効な戻り位置がなければ、 (tag value)を伴ったエラーno-catchを通知する。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.2 catchthrowの例

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Examples%20of%20Catch"
"elisp/catchthrowの例"へのコメント(無し)

catchthrowの使い方の1つは、 2重のループからの脱出です。 (ほとんどの言語では、これを『go to』で行うであろう。) ここでは、ijを0から9に変えながら、 (foo i j)を計算します。

 
(defun search-foo ()
  (catch 'loop
    (let ((i 0))
      (while (< i 10)
        (let ((j 0))
          (while (< j 10)
            (if (foo i j)
                (throw 'loop (list i j)))
            (setq j (1+ j))))
        (setq i (1+ i))))))

fooがある時点でnil以外を返すと、 ただちに止まってijのリストを返します。 fooがつねにnilを返すと、 catchは通常どおりに戻って、その値はnilです。 というのは、whileの結果はnilだからです。

2つの巧妙な例をあげましょう。 多少異なる2つの戻り位置が同時に存在します。 まず、同じタグhackで2つの戻り位置があります。

 
(defun catch2 (tag)
  (catch tag
    (throw 'hack 'yes)))
=> catch2

(catch 'hack 
  (print (catch2 'hack))
  'no)
-| yes
=> no

どちらの戻り位置もthrowに一致するタグなので、 内側のもの、つまり、catch2で確立したものに戻ります。 したがって、catch2は値yesで通常どおり戻り、 この値が表示されます。 最後に、外側のcatchの2番目の本体フォーム、 つまり、'noが評価され、外側のcatchから戻ります。

今度は、catch2に指定する引数を変更してみます。

 
(defun catch2 (tag)
  (catch tag
    (throw 'hack 'yes)))
=> catch2

(catch 'hack
  (print (catch2 'quux))
  'no)
=> yes

ここでも2つの戻り位置がありますが、 今度は外側のものだけがタグhackです。 内側のものはタグquuxです。 したがって、throwにより、外側のcatchが値yesを返します。 関数printはけっして呼ばれず、 本体フォーム'noもけっして評価されません。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.3 エラー

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Errors"
"elisp/エラー"へのコメント(無し)

Emacs Lispが、なんらかの理由で評価できないフォームを評価しようとしたときには、 Emacs Lispはエラー(error)を通知(signals)します。

エラーが通知されると、Emacsのデフォルトの動作は、 エラーメッセージを表示し、現在のコマンドの実行を終了します。 バッファの末尾でC-fを打ったときなどのように、 これはほとんどの場合、適切なことです。

複雑なプログラムでは、単に終了するだけでは満足できないこともあります。 たとえば、プログラムではデータ構造に一時的な変更を加えていたり、 プログラム終了時には削除する必要がある一時的なバッファを作成するでしょう。 そのような場合には、unwind-protectを使って、 エラー発生時に評価される後始末式(cleanup expressions)を 確立しておきます。 (see 節 9.5.4 非ローカル脱出時の後始末。) 場合によっては、サブルーティンでエラーが発生しても、 プログラムの実行を継続したいこともあるでしょう。 このような場合には、condition-caseを使って、 エラー状態から制御を回復するための エラーハンドラ(error handlers)を確立しておきます。

エラー処理を用いてプログラムのある場所から別の場所へ制御を移す、 という誘惑には耐えてください。 そのかわりにcatchthrowを使いましょう。 See 節 9.5.1 明示的な非ローカル脱出: catchthrow

9.5.3.1 エラーの通知方法    How to report an error.
9.5.3.2 Emacsのエラー処理方法    What Emacs does when you report an error.
9.5.3.3 エラーハンドラの書き方    How you can trap errors and continue execution.
9.5.3.4 エラーシンボルと条件名    How errors are classified for trapping them.



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.3.1 エラーの通知方法

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Signaling%20Errors"
"elisp/エラーの通知方法"へのコメント(無し)

ほとんどのエラーは、他の目的で呼び出したLisp関数の内部で『自動的』に 通知されます。 整数のCARを計算しようとしたり、 バッファの末尾で1文字進めようとしたりしたときなどです。 関数errorや関数signalで、 明示的にエラーを通知することもできます。

ユーザーがC-gを打ったときに発生する中断は、 エラーとは考えませんが、エラーのように扱います。

Function: error format-string &rest args
この関数は、format-stringargsformat(see 節 4.6 文字と文字列の変換)を適用して作った エラーメッセージを伴ったエラーを通知する。

errorの典型的な使い方を以下に示す。

 
(error "That is an error -- try something else")
     error--> That is an error -- try something else

(error "You have committed %d errors" 10)
     error--> You have committed 10 errors

errorは、2つの引数、 エラーシンボルerrorformatが返す文字列を含むリスト でsignalを呼び出すことで動作する。

警告: 独自のエラーメッセージをそのまま使いたい場合に、 単に(error string)とは書かないこと。 stringに`%'が含まれていると、 それは書式付け指定と解釈され、予測不能な結果を招く。 そのかわりに、(error "%s" string)を使う。

Function: signal error-symbol data
この関数は、error-symbolという名前のエラーを通知する。 引数dataは、エラーの状況に関連したLispオブジェクトのリストである。

引数error-symbolは、エラーシンボル(error symbol)である 必要がある。 つまり、属性error-conditionsを持つシンボルであり、 その属性値は条件名のリストである。 これにより、Emacsはエラーの異なる種類を分類する。

dataのオブジェクトの個数と重要性はerror-symbolに依存する。 たとえば、エラーwrong-type-argでは、 リストには2つのオブジェクトがあるはずで、 予期した型を表す述語とその型に一致しなかったオブジェクトである。 エラーシンボルの説明は、see 節 9.5.3.4 エラーシンボルと条件名

error-symboldataの両者は、 任意のエラーハンドラで利用できる。 condition-caseは、ローカル変数に フォーム(error-symbol . data)のリストを束縛する (see 節 9.5.3.3 エラーハンドラの書き方)。 エラーが処理されないと、これらの2つの値はエラーメッセージの表示に使われる。

関数signalはけっして戻らない (しかし、Emacsの古い版では戻る場合もある)。

 
(signal 'wrong-number-of-arguments '(x y))
     error--> Wrong number of arguments: x, y

(signal 'no-such-error '("My unknown error condition"))
     error--> peculiar error: "My unknown error condition"

Common Lispに関した注意: Emacsには、Common lispの継続可能なエラーの概念に相当するものはない。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.3.2 Emacsのエラー処理方法

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Processing%20of%20Errors"
"elisp/Emacsのエラー処理方法"へのコメント(無し)

エラーが通知されると、signalは、 エラーに対する有効なハンドラ(handler)を探します。 ハンドラは、Lispプログラムの一部でエラーが発生した場合に 実行されるように指定されたLisp式の列です。 エラーに対して適用可能なハンドラがあると、 そのハンドラが実行され、ハンドラに続いて制御は復旧します。 ハンドラは、そのハンドラを設定したcondition-caseの環境で実行されます。 condition-caseの内側で呼び出された関数はすべて終了しているので、 ハンドラからそれらへ戻ることはできません。

エラーに適用可能なハンドラがなければ、 現在のコマンドは終了し、制御はエディタコマンドループへ戻ります。 というのは、コマンドループには、 すべての種類のエラーに対する暗黙のハンドラがあるからです。 コマンドループのハンドラは、エラーシンボルと関連するデータを使って エラーメッセージを表示します。

明示的なハンドラがないエラーは、Lispデバッガを呼び出すこともあります。 変数debug-on-error(see 節 17.1.1 エラーによるデバッガの起動)が nil以外であると、デバッガが有効になります。 エラーハンドラと違って、デバッガはエラーの環境で実行されるので、 エラー時の変数の正確な値を調べることができます。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.3.3 エラーハンドラの書き方

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Handling%20Errors"
"elisp/エラーハンドラの書き方"へのコメント(無し)

エラーを通知することの普通の効果は、 実行中のコマンドを終了し、Emacsのエディタコマンドループにただちに戻ります。 読者のプログラムの一部で発生したエラーを捕捉するようにするには、 スペシャルフォームcondition-caseを使ってエラーハンドラを設定します。 単純な例はつぎのようになります。

 
(condition-case nil
    (delete-file filename)
  (error nil))

これはfilenameという名前のファイルを削除しますが、 エラーが発生するとどんなエラーでも捕捉してnilを返します。

condition-caseの第2引数を 保護されたフォーム(protected form)と呼びます。 (上の例では、保護されたフォームはdelete-fileの呼び出し。) このフォームの実行を開始するとエラーハンドラが有効になり、 このフォームから戻るとエラーハンドラは取り除かれます。 そのあいだは、つねにエラーハンドラは有効です。 特に、このフォームから呼び出される関数の実行中、 それらのサブルーティンの実行中などには、エラーハンドラは有効です。 これは大切なことで、厳密にいえば、 エラーが通知されるのは、保護されたフォームから呼び出された (signalerrorを含む)Lisp基本関数の実行中であって、 保護されたフォームそのものからではないからです。

保護されたフォームのうしろにある引数は、ハンドラです。 各ハンドラは1つ以上の(シンボルである)条件名 (condition names)を列挙し、処理するエラーを指定します。 エラーが通知されたときのエラーシンボルも条件名のリストを定義します。 それらに共通の条件名があるとき、 エラーハンドラがエラーに適用されます。 上の例では、1つのハンドラがあり、条件名は1つ、errorを指定しています。 この条件名はすべてのエラーを意味します。

適用可能なハンドラの探索では、 もっとも最近に確立されたハンドラから始めて、 確立されたすべてのハンドラを調べます。 したがって、フォームcondition-caseが2つ入れ子になっていて 同じ名前のハンドラを確立していると、内側のものが実際に処理を受け持ちます。

フォームcondition-caseでエラーが処理されるときには、 debug-on-errorでエラーによりデバッガを起動するように指定してあっても デバッガは実行されません。 See 節 17.1.1 エラーによるデバッガの起動condition-caseで捕捉されるエラーをデバッグしたいときには、 変数debug-on-signalnil以外の値を設定します。

エラーを処理できる場合には、制御はハンドラに移ります。 こうするまえに、Emacsは、抜け出し対象となる束縛作成構造が設定した すべての変数束縛を解き、抜け出し対象となるフォームunwind-protect すべての後始末を実行します。 ハンドラに制御が移ると、ハンドラの本体を実行します。

ハンドラ本体の実行を完了すると、 フォームcondition-caseから戻ります。 ハンドラを実行するまえに保護されたフォームから完全に抜けているので、 ハンドラでは、エラー発生時点から再開したり、 保護されたフォームの内側で作られた変数束縛を調べたりすることはできません。 ハンドラでできることは、後始末をして先へ進むことだけです。

condition-case構造は、insert-file-contentsの呼び出しで ファイルのオープンに失敗するなどの予測可能なエラーを捕捉するために しばしば使われます。 プログラムがユーザーから読み取った式を評価する場合のように、 まったく予測不可能なエラーを捕捉するためにも使われます。

エラー通知とエラー処理は、throwcatchに多少似ていますが、 それらはまったく別の機能です。 catchではエラーを捕捉できませんし、 エラーハンドラではthrowを処理できません (しかしながら、適切なcatchがないthrowを使うと、 処理できるエラーを通知する)。

Special Form: condition-case var protected-form handlers...
このスペシャルフォームは、protected-formの実行中は エラーハンドラhandlersを確立する。 protected-formがエラーなしに完了すると、 その戻り値がフォームcondition-caseの値になる。 この場合、condition-caseはなんの効果もない。 フォームcondition-caseで違いがでるのは、 protected-formの実行中にエラーが起こった場合である。

handlersは、(conditions body...)の形式の リストである。 ここでconditionsは、処理すべきエラーの条件名か条件名のリストである。 bodyは1つ以上のLisp式であり、 このハンドラがエラーを処理するときに実行される。 ハンドラの例を示す。

 
(error nil)

(arith-error (message "Division by zero"))

((arith-error file-error)
 (message
  "Either division by zero or failure to open a file"))

生起する各エラーには、 そのエラーの種類を表すエラーシンボル(error symbol)がある。 そのシンボルの属性error-conditionsは、 条件名のリストである(see 節 9.5.3.4 エラーシンボルと条件名)。 Emacsは、有効なフォームcondition-caseすべてを探索し、 これらの条件名を1つ以上指定したハンドラを探す。 もっとも内側の一致するcondition-caseがエラーを処理する。 このcondition-caseの内側では、 適用可能な最初のハンドラがエラーを処理する。

ハンドラの本体の実行を完了すると、 condition-caseは通常のように戻り、 ハンドラの本体の最後のフォームの値を全体としての値に使う。

引数varは変数である。 condition-caseは、protected-formを実行するときには この変数を束縛せず、エラーを処理するときだけ束縛する。 そのとき、varはローカルにエラー記述 (error description)に束縛される。 これは、エラーの詳細を与えるリストである。 エラー記述は、(error-symbol . data)の形式である。 ハンドラは、動作を決定するためにこのリストを参照できる。 たとえば、ファイルのオープンに失敗したエラーであれば、 dataの第2要素、エラー記述の第3要素がファイル名である。

varnilであると、変数を束縛しなことを意味する。 そうすると、ハンドラではエラーシンボルと関連するデータを使えない。

Function: error-message-string error-description
この関数は、指定したエラー記述に対するエラーメッセージ文字列を返す。 エラーに対する普通のエラーメッセージを表示して、 エラーを処理したい場合に便利である。

ゼロ除算の結果であるエラーを処理するcondition-caseの使用例を示します。 ハンドラはエラーメッセージを(ベルを鳴らさずに)表示して、 大きな数を返します。

 
(defun safe-divide (dividend divisor)
  (condition-case err                
      ;; 保護されたフォーム
      (/ dividend divisor)              
    ;; ハンドラ
    (arith-error                        ; 条件
     ;; このエラーに対する普通のメッセージを表示する
     (message "%s" (error-message-string err))
     1000000)))
=> safe-divide

(safe-divide 5 0)
     -| Arithmetic error: (arith-error)
=> 1000000

ハンドラは条件名arith-errorを指定しているので、 ゼロ除算エラーだけを処理します。 少なくともこのcondition-caseでは他の種類のエラーは処理しません。 したがって、つぎのようになります

 
(safe-divide nil 3)
     error--> Wrong type argument: number-or-marker-p, nil

以下は、errorで通知されるエラーも含めて、 すべての種類のエラーを捕捉するcondition-caseです。

 
(setq baz 34)
     => 34

(condition-case err
    (if (eq baz 35)
        t
      ;; これは関数errorの呼び出し
      (error "Rats!  The variable %s was %s, not 35" 'baz baz))
  ;; これはハンドラ。フォームではない
  (error (princ (format "The error was: %s" err)) 
         2))
-| The error was: (error "Rats!  The variable baz was 34, not 35")
=> 2



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.3.4 エラーシンボルと条件名

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Error%20Symbols"
"elisp/エラーシンボルと条件名"へのコメント(無し)

エラーを通知するときには、読者が意図するエラーの種類を指定する エラーシンボル(error symbol)を指定します。 各エラーには、それを分類する一意な名前があります。 これは、Emacs Lisp言語で定義されたエラーを細分類したものです。

これらの細分類は、エラー条件(error conditions)と呼ばれる より大きなクラスの階層にまとめられています。 エラー条件は、条件名(condition names)で識別します。 もっとも細かい分類は、エラーシンボルそのものです。 各エラーシンボルは条件名でもあります。 より大きなクラスを表す条件名errorもあります。 これはすべての種類のエラーを表します。 したがって、各エラーには、1つ以上の条件名があります。 つまり、errorerrorとは別のエラーシンボル、あるいは、 その中間の分類に属するものです。

あるシンボルがエラーシンボルであるためには、そのシンボルには、 条件名のリストを与える属性error-conditionsがあることが必要です。 このリストは、そのエラーが属するエラー条件を定義します。 (エラーシンボルそのものと、シンボルerrorは、 つねにこのリストの要素であること。) したがって、条件名の階層は、 エラーシンボルの属性error-conditionsで定義されます。

error-conditionsリストに加えて、 エラーシンボルには、属性error-messageも必要です。 この属性の値は、そのエラーが処理されないときに表示される文字列です。 属性error-messageがあるのに、それが文字列でなければ、 エラーメッセージ`peculiar error'を使います。

以下に、新たなエラーシンボルnew-errorの定義方法を示します。

 
(put 'new-error
     'error-conditions
     '(error my-own-errors new-error))       
=> (error my-own-errors new-error)
(put 'new-error 'error-message "A new error")
=> "A new error"

このエラーには、3つの条件名があります。 もっとも細かい分類であるnew-error、 それより大きな分類とであると考えているmy-own-error、 もっとも大きな分類であるerrorです。

エラー文字列は大文字で始めるべきですが、ピリオドで終えません。 これは、Emacsの他の慣習と整合をとるためです。 普通、Emacs自身がnew-errorを通知することはありえません。 つぎのように、読者のコードで明示的に signal(see 節 9.5.3.1 エラーの通知方法)を呼んだときだけです。

 
(signal 'new-error '(x y))
     error--> A new error: x, y

このエラーは、3つの条件名のどれでも処理できます。 つぎの例は、new-errorと クラスmy-own-errorsの任意の他のエラーを処理します。

 
(condition-case foo
    (bar nil t)
  (my-own-errors nil))

エラーを分類する重要な方法は、それらの条件名によることです。 つまり、エラーに一致するハンドラを探すために条件名を使います。 エラーシンボルは、意図したエラーメッセージと条件名のリストを指定する 簡便な方法を提供するだけです。 signalに、1つのエラーシンボルではなく、 条件名のリストを指定するのではわずらわしいでしょう。

一方、条件名なしにエラーシンボルだけを使うのでは、 condition-caseの能力をいちじるしく損ないます。 条件名があることで、エラーハンドラを書くときにさまざまなレベルに 一般化してエラーを分類できるのです。 エラーシンボルだけを使ったのでは、 最細分類以外のレベルを削除してしまうことになります。

すべての標準エラー名とそれらの条件名については、 See 節 C. 標準のエラー



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

9.5.4 非ローカル脱出時の後始末

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp&node=Cleanups"
"elisp/非ローカル脱出時の後始末"へのコメント(無し)

unwind-protect構造は、データ構造を一時的に整合性のない状態に するときには本質的です。 この構造により、エラーや非ローカル脱出が起こったときに、 データの整合性を回復できます。

Special Form: unwind-protect body cleanup-forms...
unwind-protectは、bodyからどのように制御が離れた場合にも cleanup-formsの実行を保証して、bodyを実行する。 bodyは通常どおり完了するか、 throwを実行してunwind-protectから脱出するか、 エラーを引き起こす。 いずれの場合でも、cleanup-formsは評価される。

フォームbodyが正常に終了すると、 unwind-protectは、cleanup-formsを評価したあとに、 フォームbodyの最後の値を返す。 フォームbodyが完了しなかった場合、 unwind-protectは普通の意味での値は返さない。

unwind-protectが保護するのはbodyだけである。 cleanup-formsそのもののどれかが(throwやエラーで) 非ローカル脱出を行うと、unwind-protectは、 cleanup-formsの残りを評価することを保証しないcleanup-formsのどれかが失敗するとトラブルになる危険性がある場合には、 cleanup-formsを別のunwind-protectで保護する。

フォームunwind-protectの現在の入れ子の個数は、 ローカル変数束縛の個数とともに数えられ、 max-specpdl-sizeに制限されている(see 節 10.3 ローカル変数)。

たとえば、表示しないバッファを一時的に作成し、 終了前に確実にそれを消去したいとしましょう。

 
(save-excursion
  (let ((buffer (get-buffer-create " *temp*")))
    (set-buffer buffer)
    (unwind-protect
        body
      (kill-buffer buffer))))

変数bufferを使わずに(kill-buffer (current-buffer))と 書くだけで十分だと考えるかもしれません。 しかし、別のバッファに切り替えたあとでbodyでエラーが発生した場合には、 上の方法はより安全です。 (あるいは、bodyの周りに別のsave-excursionを書いて、 一時バッファを消去するときに、それがカレントバッファになることを 保証する。)

Emacsには、上のようなコードに展開されるwith-temp-bufferという 標準マクロがあります(see 節 26.2 カレントバッファ)。 本書で定義しているマクロのいくつかでは、 このようにunwind-protectを使っています。

ファイル`ftp.el'から持ってきた実際の例を示しましょう。 リモートの計算機への接続を確立するプロセス(see 節 36. プロセス)を作ります。 関数ftp-loginは、その関数の作成者が予想できないほどの 数多くの問題に対してとても敏感ですから、 失敗したときにプロセスを消去することを保証するフォームで保護します。 さもないと、Emacsは、無用なサブプロセスで満たされてしまいます。

 
(let ((win nil))
  (unwind-protect
      (progn
        (setq process (ftp-setup-buffer host file))
        (if (setq win (ftp-login process host user password))
            (message "Logged in")
          (error "Ftp login failed")))
    (or win (and process (delete-process process)))))

この例には、小さなバグが1つあります。 ユーザーがC-gを打って中断しようとして、かつ、 関数ftp-setup-bufferの終了後に 変数processを設定するまえに実際に中断が行われると、 プロセスは消去されません。 このバグを直す簡単な方法はありませんが、 少なくとも、ほとんど起こりえません。


[ << ] [ >> ]           [表紙] [目次] [索引] [検索] [上端 / 下端] [?]