Next: , Up: パターンマッチングによる条件   [Contents][Index]


11.4.1 pcaseマクロ

背景はパターンマッチングによる条件を参照してください。

Macro: pcase expression &rest clauses

clauses内のclauseは(pattern body-forms…)という形式をもつ。

expressionの値( expval )を決定するために評価する。patternexpvalにマッチするような最初のclauseを探して、そのclauseのbody-formsに制御を渡す。

マッチが存在すればpcaseの値はマッチが成功したclauseのbody-formsの最後の値、それ以外ならpcasenilに評価される。

patternpcaseパターン(pcase pattern)である必要があります。これは以下に定義されるコアパターンのいずれか、またはpcase-defmacro (pcaseの拡張を参照)を通じて定義されるパターンの1つを使用できます。

このサブセクションの残りの部分ではコアパターンの異なるフォームをいくつかの例を交えて説明して、いくつかのパターンフォームが提供するletバイディング機能を使用する上で重要な注意点で締めくくります。コアパターンは以下のフォームをもつことができます:

_ (アンダースコア)

任意のexpvalにマッチ。これはdon’t care、あるいはワイルドカード(wildcard)としても知られる。

'val

expvalvalが等しければマッチ。比較はequalのように行われる(同等性のための述語を参照)。

keyword
integer
string

expvalがリテラルオブジェクトと等しければマッチ。これは上述の'valの特殊なケースであり、これらのタイプのリテラルオブジェクトが自己クォート(self-quoting)を行うために可能となる。

symbol

任意のexpvalにマッチするとともに、追加でsymbolexpvalにletバインドする。このようなバインディングはbody-forms内でも利用できる(ダイナミックバインディングを参照)。

symbolが(以下のandを使用することにより)シーケンスパターンseqpatの一部なら、symbolに後続するseqpat部分でもバインディングは利用可能。この使用法にはいくつかの注意点がある。caveatsを参照のこと。

使用を避けるべき2つのシンボルは_ (上述)と同様に振る舞う非推奨のtnil(エラーをシグナルする)。同様にキーワードシンボルへのバインドは無意味(変更不可な変数を参照)。

`qpat

バッククォートスタイルのパターン。詳細についてはバッククォートスタイルパターンを参照のこと。

(cl-type type)

expvalがタイプtype ( cl-typepが許す型記述子。 Type Predicates in Common Lisp Extensionsを参照)ならマッチ。たとえば:

(cl-type integer)
(cl-type (integer 0 10))
(pred function)

expvalにたいして述語functionが非nilをリターンしたらマッチ。このテストは構文(pred (not function))で否定となる。述語functionは以下のフォームのいずれかが可能:

関数名(シンボル)

expvalを単一の引数として名前つきの関数を呼び出す。

例: integerp

lambda式

expvalを単一の引数として無名関数を呼び出す(ラムダ式を参照)。

例: (lambda (n) (= 42 n))

n個の引数での関数呼び出し

関数(関数呼び出しの1つ目の要素)をn個の引数(残りの要素)、およびexpvaln+1番目の追加の引数としてで呼び出す。

例: (= 42)
この例では関数が=nが1であり、実際の関数呼び出しは(= 42 expval)になる。

(app function pattern)

expvalにたいして呼び出したfunctionpatternにマッチする値をリターンすればマッチ。functionは上述のpredで説明したフォームのいずれかが可能。しかしpredとは異なり、appはブーリーンの真値ではなくpatternにたいして結果をテストする点が異なる。

(guard boolean-expression)

boolean-expressionが非nilに評価されればマッチ。

(let pattern expr)

exprvalを取得するためにexprを評価して、exprvalpatternにマッチすればマッチ(patternsymbolを使用することでシンボルに値をバインドできるのでletと呼ばれる)。

seqpatとしても知られるシーケンスパターン(sequencing pattern)はサブパターンを順に処理するパターンです。pcaseにたいしてはandorの2つが存在します。これらは同じ名前のスペシャルフォーム(組み合わせ条件の構築を参照)と同様の方式で振る舞いますが、値ではなくサブパターンを処理します。

(and pattern1…)

pattern1…のいずれかがマッチに失敗するまで順にマッチを試みる。この場合にはandもマッチに失敗して、残りのサブパターンはテストしない。すべてのサブパターンがマッチすればandはマッチする。

(or pattern1 pattern2…)

pattern1pattern2、…のいずれかがマッチに成功するまで順にマッチを試みる。この場合にはorもマッチして、残りのサブパターンはテストしない。

body-formsにたいして一貫した環境 (評価の概要を参照) を与える(マッチでの評価エラーを回避できる)ために、パターンがバインドする変数セットは各サブパターンがバインドする変数を結合したものになる。ある変数がマッチしたサブパターンにバインドされない場合にはnilにバインドされる。

(rx rx-expr…)

string-matchが行うようにrx正規表現表記を使用して文字列を正規表現rx-expr…にマッチする(rx構造化Rgexp表記を参照)。

通常のrx構文に加えて、rx-expr…には以下の構文を含めることができる:

(let ref rx-expr…)

シンボルrefrx-expr...にマッチする部分マッチにバインドする。refbody-forms内で部分マッチの文字列かnilにバインドされるが、backrefでも使用可能。

(backref ref)

標準のbackref構文と同様だが、refは前の(let ref …)構文で導入された名前にもなる。

例: cl-caseにたいする利点

以下はcl-case (Conditionals in Common Lisp Extensionsを参照)にたいするpcaseの利点のいくつかを強調する例です。

(pcase (get-return-code x)
  ;; 文字列
  ((and (pred stringp) msg)
   (message "%s" msg))
  ;; symbol
  ('success       (message "Done!"))
  ('would-block   (message "Sorry, can't do it now"))
  ('read-only     (message "The schmilblick is read-only"))
  ('access-denied (message "You do not have the needed rights"))
  ;; default
  (code           (message "未知のリターンコード %S" code)))

cl-caseではget-return-codeのリターン値を保持するためにローカル変数codeを宣言する必要があります。さらにcl-caseは比較にeqlを使用するので文字列の使用も難しくなります。

例: andの使用

後続のサブパターン(と同様にbodyフォーム)にバインディングを提供する1つ以上のsymbolサブパターンをもつandで開始するのが、パターンを記述する際の一般的なイディオムです。たとえば以下のパターンは1桁の整数にマッチします。

(and
  (pred integerp)
  n                     ; nexpvalをバインド
  (guard (<= -9 n 9)))

まずpred(integerp expval)が非nilに評価されればマッチになります。次にnはすべてにマッチするsymbolパターンであり、nexpvalがバインドされます。最後にguardはブーリーン式(<= -9 n 9)が非nilに評価されればマッチになります(nへの参照に注意)。これらすべてのサブパターンがマッチすればandがマッチになります。

例: pcaseによる書き換え

以下はシンプルなマッチングタスクを伝統的な実装(関数grok/traditionalから、pcaseを使用する実装(関数grok/pcase)に書き換える別の例です。これらの関数のdocstringはいずれも“If OBJ is a string of the form "key:NUMBER", return NUMBER (a string). Otherwise, return the list ("149" default).”です。最初は伝統的な実装です(正規表現を参照):

(defun grok/traditional (obj)
  (if (and (stringp obj)
           (string-match "^key:\\([[:digit:]]+\\)$" obj))
      (match-string 1 obj)
    (list "149" 'default)))

(grok/traditional "key:0")   ⇒ "0"
(grok/traditional "key:149") ⇒ "149"
(grok/traditional 'monolith) ⇒ ("149" default)

この書き換えではsymbolバインディング、およびorandpredappletを実演します。

(defun grok/pcase (obj)
  (pcase obj
    ((or                                     ; L1
      (and                                   ; L2
       (pred stringp)                        ; L3
       (pred (string-match                   ; L4
              "^key:\\([[:digit:]]+\\)$"))   ; L5
       (app (match-string 1)                 ; L6
            val))                            ; L7
      (let val (list "149" 'default)))       ; L8
     val)))                                  ; L9

(grok/pcase "key:0")   ⇒ "0"
(grok/pcase "key:149") ⇒ "149"
(grok/pcase 'monolith) ⇒ ("149" default)

grok/pcaseは主に1つのpcaseフォームのclause、L1からL8のパターン、L9の(1つの)フォームからなります。パターンは引数であるサブパターンにたいして順にマッチを試みるorです。これは最初にand (L2からL7)、次にlet (L8)のように、いずれかが成功するまで順にマッチを試みます。

前出の例(Example 1を参照)のように、andは以降のサブパターンが正しいタイプのオブジェクト(この場合は文字列)に作用することを保証するためにpredサブパターンで始まります。(stringp expval)nilならpredは失敗となり、したがってandも失敗となります。

次のpred (L4からL5)は(string-match RX expval)を評価して結果が非nil ( expvalが期待するフォームkey:NUMBERであることを意味する)ならマッチになります。これが失敗すると再びpredは失敗となり、したがってandも失敗となります。

(andの一連のサブパターンでは)最後のappは一時的な値tmp (部分文字列 “NUMBER”)を取得するために(match-string 1 expval) (L6)を評価して、パターンval (L7)とtmpのマッチを試みます。これはsymbolパターンなので無条件でマッチして、valtmpを追加でバインドします。

ついにappがマッチしたので、andのすべてのサブパターンがマッチして、andがマッチとなります。同じように一度andがマッチすればorがマッチするので、サブパターンlet (L8)の処理が試みられることはありません。

objが文字列以外、あるいは間違った形式の文字列の場合を考えてみましょう。この場合にはいずれかのpred (L3からL5)がマッチに失敗するので、and (L2)がマッチに失敗して、or (L1)がサブパターンlet (L8)の処理を試みます。

まずlet("149" default)を取得するために(list "149" 'default)を評価して、それからパターンvalにたいしてexprvalのマッチを試みます。これはsymbolパターンなので無条件にマッチして、追加でvalexprvalをバインドします。これでletがマッチしたので、orがマッチとなります。

andletのサブパターンはどちらも同じ方法、すなわちvalをバインドする過程でsymbolパターンのvalに(常に成功する)マッチを試みることにより完了することに注意してください。したがってorは常にマッチして、常に制御をbodyフォーム(L9)に渡します。マッチが成功したpcaseのclauseとしては最後のbodyなので、これはpcaseの値となり、同様にgrok/pcaseのリターン値になります(関数とは?を参照)。

シーケンスパターンにおけるsymbolの注意点

前出の例のすべてでは、何らかの方法によりsymbolサブパターンが含まれるシーケンスパターンが使用されています。以下に使用法に関する重要な詳細をいくつか挙げます。

  1. seqpat内にsymbolが複数回出現する場合には、 2回目以降に出現してもリバインドには展開されないが、かわりにeqを使用した等価性テストに展開される。

    以下の例には2つのclauseと2つのseqpat (AとB)を使用している。AとBはいずれも最初に(predを使用することにより)expvalがペアであることをチェックして、それから(それぞれにたいしてappを使用することにより) expvalcarcdrにシンボルをバインドする。

    Aではシンボルstが2回記述されているので、2つ目の記述はeqを使用した等価性チェックになる。一方でBはs1s2という別個のシンボルを使用するので、独立したバインディングになる。

    (defun grok (object)
      (pcase object
        ((and (pred consp)        ; seqpat A
              (app car st)        ; st: 1回目
              (app cdr st))       ; st: 2回目
         (list 'eq st))
    
        ((and (pred consp)        ; seqpat B
              (app car s1)        ; s1: 1回目
              (app cdr s2))       ; s2: 1回目
         (list 'not-eq s1 s2))))
    
    
    
    (let ((s "yow!"))
      (grok (cons s s)))      ⇒ (eq "yow!")
    (grok (cons "yo!" "yo!")) ⇒ (not-eq "yo!" "yo!")
    (grok '(4 2))             ⇒ (not-eq 4 (2))
    
  2. symbolを参照するコードの副作用は未定義。 無視する。たとえば以下2つの関数は類似している。いずれの関数もandsymbolguardを使用する:
    (defun square-double-digit-p/CLEAN (integer)
      (pcase (* integer integer)
        ((and n (guard (< 9 n 100))) (list 'yes n))
        (sorry (list 'no sorry))))
    
    (square-double-digit-p/CLEAN 9) ⇒ (yes 81)
    (square-double-digit-p/CLEAN 3) ⇒ (no 9)
    
    
    
    (defun square-double-digit-p/MAYBE (integer)
      (pcase (* integer integer)
        ((and n (guard (< 9 (incf n) 100))) (list 'yes n))
        (sorry (list 'no sorry))))
    
    (square-double-digit-p/MAYBE 9) ⇒ (yes 81)
    (square-double-digit-p/MAYBE 3) ⇒ (yes 9)  ; WRONG!
    

    違いはguard内のboolean-expressionである。CLEANは単に直接nを参照するのにたいして、MAYBEは式(incf n)の中で副作用によりnを参照している。integerの際には以下のようなことが発生している:

    • 最初のnexpval (評価した結果である(* 3 3)、つまり9)にバインドされる。
    • boolean-expressionが評価される:
      start:   (< 9 (incf n)        100)
      becomes: (< 9 (setq n (1+ n)) 100)
      becomes: (< 9 (setq n (1+ 9)) 100)
      
      becomes: (< 9 (setq n 10)     100)
                                         ; ここで副作用!
      becomes: (< 9       n         100) ; nは10にバインドされている
      becomes: (< 9      10         100)
      becomes: t
      
    • 結果は非nilなので guardがマッチしてandがマッチとなり、制御はそのclauseのbodyフォームに渡される。

    MAYBEには9が2桁の整数だと判定してしまう数学的な誤り以外にも問題がある。bodyフォームはnの更新された値(10)を確認せずに参照を複数回行う。するとどうなるか?

    要約すると( guardでの) boolean-expressionだけではなく( letでの) expr、( predappでの) functionでも副作用をもつsymbolパターンにたいする参照は完全に避けることが最良である。

  3. マッチではclauseのbodyフォームはletバインドされたパターンのシンボルセットを参照できる。 このシンボルセットはseqpatandの際には、各サブパターンでletバインドされるすべてのシンボルそれぞれを結合したものになる。andのマッチではすべてのサブパターンがマッチしなければならないので、これには意味がある。

    seqpatorなら事情は異なる。orは最初にマッチしたサブパターンでマッチとなり、残りのサブパターンは無視される。bodyフォームにはどのサブパターンがマッチして異なるセットの中からどれが選択されたかを知る術はないので、各シンボルが異なるシンボルセットをletバインドすることに意味はない。たとえば以下は無効:

    (require 'cl-lib)
    (pcase (read-number "Enter an integer: ")
      ((or (and (pred cl-evenp)
                e-num)      ; e-numexpvalにバインド
           o-num)           ; o-numexpvalにバインド
       (list e-num o-num)))
    
    
    
    Enter an integer: 42
    error→ Symbol’s value as variable is void: o-num
    
    Enter an integer: 149
    error→ Symbol’s value as variable is void: e-num
    

    bodyフォーム(list e-num o-num)の評価によりエラーがシグナルされる。サブパターンを区別するために、すべてのサブパターンごとに異なる値をもつ同一名のシンボルを使用できる。上記の例を書き換えると:

    (require 'cl-lib)
    (pcase (read-number "Enter an integer: ")
      ((and num                                ; L1
            (or (and (pred cl-evenp)           ; L2
                     (let spin 'even))         ; L3
                (let spin 'odd)))              ; L4
       (list spin num)))                       ; L5
    
    
    
    Enter an integer: 42
    ⇒ (even 42)
    
    Enter an integer: 149
    ⇒ (odd 149)
    

    L1ではexpvalのバインディング(この場合はnum )をandsymbolで“分解”している。L2では前と同じ方法でorは始まるが、異なるシンボルにバインドするかわりに、両方のサブパターン内で同一のシンボルspinに回バインドするために2回letを使用している(L3からL4)。spinの値によりサブパターンは区別される。そしてbodyフォームでは両方のシンボルを参照している(L5)。


Next: pcaseの拡張, Up: パターンマッチングによる条件   [Contents][Index]