11.4.1 Макрос pcase

Для общей информации см. Условное сопоставление с образцом.

Макрос: pcase выражение &rest ветви

Каждая ветвь в ветвях имеет вид: (образец тело-формы…).

Вычислить выражение, чтобы определить его значение, знач-выр. Найти первую ветвь в ветвях, чей образец сопоставляется с знач-выр, и передать управление тело-формам этой ветви.

Если сопоставление найдено, значением pcase является значение последней из тело-форм в успешной ветви. В противном случае pcase вычисляется в nil.

Каждый образец должен быть образцом pcase, который может использовать либо один из основных образцов, определённых ниже, либо один из образцов, определённых через pcase-defmacro (см. Расширение pcase).

В остальной части этого подраздела описываются различные формы основных образцов, приводятся некоторые примеры и в заключение рассматриваются важные предостережения относительно использования механизма связывания let, предоставляемого некоторыми формами образцов. Основной образец может иметь следующие формы:

_ (подчёркивание)

Сопоставляется с любым знач-выр. Это также известно как всё равно или шаблон.

'знач

Сопоставляется, если знач-выр равно знач. Сравнение выполняется как с помощью equal (см. Предикаты равенства).

ключевое-слово
целое
строка

Сопоставляется, если знач-выр равно литеральному объекту. Это частный случай 'знач, описанного выше, возможный, поскольку литеральные объекты этих типов являются самоцитируемыми.

символ

Сопоставляется с любым знач-выр и дополнительно связывает символ с знач-выр с помощью let, так что эта привязка доступна в тело-формах (см. Динамическая привязка).

Если символ является частью последовательного образца посл-обр (например, с использованием and ниже), привязка также доступна в той части посл-обр, которая следует за появлением символа. Это использование имеет некоторые предостережения, см. предостережения.

Двух символов следует избегать: t, который ведёт себя как _ (выше) и устарел, и nil, который вызывает ошибку. Аналогично, не имеет смысла связывать символы-ключевые слова (см. Переменные, которые никогда не изменяются).

`кв-обр

Образец в стиле обратного апострофа. Подробности см. Образцы в стиле обратного апострофа.

(cl-type тип)

Сопоставляется, если знач-выр имеет тип тип, который является дескриптором типа, принимаемым cl-typep (см. Предикаты типов в Расширениях Common Lisp). Примеры:

(cl-type integer)
(cl-type (integer 0 10))
(pred функция)

Сопоставляется, если предикат функция возвращает не-nil при вызове с знач-выр. Проверка может быть инвертирована с помощью синтаксиса (pred (not функция)). Предикат функция может иметь одну из следующих форм:

имя функции (символ)

Вызвать именованную функцию с одним аргументом, знач-выр.

Пример: integerp

лямбда-выражение

Вызвать анонимную функцию с одним аргументом, знач-выр (см. Лямбда-выражения).

Пример: (lambda (n) (= 42 n))

вызов функции с n аргументами

Вызвать функцию (первый элемент вызова функции) с n аргументами (остальные элементы) и дополнительным n+1-м аргументом, которым является знач-выр.

Пример: (= 42)
В этом примере функцией является =, n равно единице, и фактический вызов функции становится: (= 42 знач-выр).

вызов функции с аргументом _

Вызвать функцию (первый элемент вызова функции) с указанными аргументами (остальные элементы), заменяя _ на знач-выр.

Пример: (gethash _ memo-table) В этом примере функцией является gethash, и фактический вызов функции становится: (gethash знач-выр memo-table).

(app функция образец)

Сопоставляется, если функция, вызванная с знач-выр, возвращает значение, которое сопоставляется с образец. функция может принимать одну из форм, описанных для pred выше. Однако, в отличие от pred, app проверяет результат на соответствие образец, а не на булево значение истинности.

(guard булево-выражение)

Сопоставляется, если булево-выражение вычисляется в не-nil.

(let образец выр)

Вычисляет выр для получения знач-выр2 и сопоставляется, если знач-выр2 сопоставляется с образец. (Названо let, потому что образец может связывать символы со значениями с помощью символ.)

Последовательный образец (также известный как посл-обр) — это образец, который обрабатывает свои под-образцы-аргументы последовательно. Их два для pcase: and и or. Они ведут себя аналогично специальным формам, разделяющим их имена (см. Конструкции для комбинирования условий), но вместо обработки значений они обрабатывают под-образцы.

(and образец1…)

Пытается сопоставить образец1… по порядку, пока один из них не сопоставится неудачно. В этом случае and также не сопоставляется, и остальные под-образцы не проверяются. Если все под-образцы сопоставляются, and сопоставляется.

(or образец1 образец2…)

Пытается сопоставить образец1, образец2, … по порядку, пока один из них не завершится успешно. В этом случае or также сопоставляется, и остальные под-образцы не проверяются.

Чтобы предоставить согласованное окружение (см. Введение в вычисление) для тело-форм (тем самым избегая ошибки вычисления при сопоставлении), набор переменных, связываемых образцом, является объединением переменных, связываемых каждым под-образцом. Если переменная не связана под-образцом, который сопоставился, то она связывается с nil.

(rx rx-выр…)

Сопоставляет строки с регулярным выражением rx-выр…, используя нотацию регулярных выражений rx (см. Структурированная нотация регулярных выражений rx), как если бы с помощью string-match.

В дополнение к обычному синтаксису rx, rx-выр… может содержать следующие конструкции:

(let ссылка rx-выр…)

Связать символ ссылка с подстрокой, которая сопоставляется с rx-выр.... ссылка связывается в тело-формах со строкой подстроки или nil, но также может использоваться в backref.

(backref ссылка)

Как стандартная конструкция backref, но ссылка здесь также может быть именем, введённым предыдущей конструкцией (let ссылка …).

Пример: преимущество перед cl-case

Вот пример, который подчёркивает некоторые преимущества pcase перед cl-case (см. Условные конструкции в Расширениях Common Lisp).

(pcase (get-return-code x)
  ;; строка
  ((and (pred stringp) msg)
   (message "%s" msg))
  ;; символ
  ('success       (message "Готово!"))
  ('would-block   (message "Извините, сейчас не могу"))
  ('read-only     (message "Шмильблик доступен только для чтения"))
  ('access-denied (message "У вас нет необходимых прав"))
  ;; по умолчанию
  (code           (message "Неизвестный код возврата %S" code)))

С cl-case вам пришлось бы явно объявить локальную переменную code для хранения возвращаемого значения get-return-code. Кроме того, cl-case сложно использовать со строками, потому что он использует eql для сравнения.

Пример: использование and

Распространённый приём — записывать образец, начинающийся с and, с одним или несколькими под-образцами символ, предоставляющими привязки для последующих под-образцов (а также для тело-форм). Например, следующий образец сопоставляется с однозначными целыми числами.

(and
  (pred integerp)
  n                     ; связать n с знач-выр
  (guard (<= -9 n 9)))

Сначала pred сопоставляется, если (integerp знач-выр) вычисляется в не-nil. Затем n — это образец символ, который сопоставляется с чем угодно и связывает n с знач-выр. Наконец, guard сопоставляется, если булево выражение (<= -9 n 9) (обратите внимание на ссылку на n) вычисляется в не-nil. Если все эти под-образцы сопоставляются, and сопоставляется.

Пример: переформулировка с pcase

Вот ещё один пример, показывающий, как переформулировать простую задачу сопоставления из её традиционной реализации (функция grok/traditional) в реализацию с использованием pcase (функция grok/pcase). Документация для обеих функций: «Если OBJ — строка вида "key:NUMBER", вернуть NUMBER (строку). В противном случае вернуть список ("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)

Переформулировка демонстрирует связывание символ, а также or, and, pred, app и let.

(defun grok/pcase (obj)
  (pcase obj
    ((or                                     ; строка 1
      (and                                   ; строка 2
       (pred stringp)                        ; строка 3
       (pred (string-match                   ; строка 4
              "^key:\\([[:digit:]]+\\)$"))   ; строка 5
       (app (match-string 1)                 ; строка 6
            val))                            ; строка 7
      (let val (list "149" 'default)))       ; строка 8
     val)))                                  ; строка 9

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

Основная часть grok/pcase — это одна ветвь формы pcase, образец в строках 1–8 и (единственная) тело-форма в строке 9. Образец — это or, который пытается сопоставить по очереди свои под-образцы-аргументы: сначала and (строки 2–7), затем let (строка 8), пока один из них не завершится успешно.

Как и в предыдущем примере (см. Пример 1), and начинается с под-образца pred, чтобы гарантировать, что следующие под-образцы работают с объектом правильного типа (в данном случае строки). Если (stringp знач-выр) возвращает nil, pred не сопоставляется, и, следовательно, and также не сопоставляется.

Следующий pred (строки 4–5) вычисляет (string-match RX знач-выр) и сопоставляется, если результат не равен nil, что означает, что знач-выр имеет нужную форму: key:NUMBER. Опять же, если это не так, pred не сопоставляется и and — тоже.

Наконец (в этой серии под-образцов and), app вычисляет (match-string 1 знач-выр) (строка 6) для получения временного значения врем (т.е. подстроки «NUMBER») и пытается сопоставить врем с образцом val (строка 7). Поскольку это образец символ, он сопоставляется безусловно и дополнительно связывает val с врем.

Теперь, когда app сопоставился, все под-образцы and сопоставились, и, значит, and сопоставляется. Аналогично, как только and сопоставился, or сопоставляется и не переходит к попытке под-образца let (строка 8).

Рассмотрим ситуацию, когда obj не является строкой или является строкой, но имеет неправильную форму. В этом случае один из pred (строки 3–5) не сопоставляется, следовательно, and (строка 2) не сопоставляется, следовательно, or (строка 1) переходит к попытке под-образца let (строка 8).

Сначала let вычисляет (list "149" 'default) для получения ("149" default), знач-выр2, а затем пытается сопоставить знач-выр2 с образцом val. Поскольку это образец символ, он сопоставляется безусловно и дополнительно связывает val с знач-выр2. Теперь, когда let сопоставился, or сопоставляется.

Обратите внимание, как под-образцы and и let завершаются одинаково: попыткой (всегда успешной) сопоставить образец символ val, связывая при этом val. Таким образом, or всегда сопоставляется, и управление всегда передаётся тело-форме (строка 9). Поскольку это последняя тело-форма в успешно сопоставленной ветви pcase, она является значением pcase и, соответственно, возвращаемым значением grok/pcase (см. Что такое функция?).

Предостережения для символ в последовательных образцах

Предыдущие примеры используют последовательные образцы, которые так или иначе включают под-образец символ. Вот некоторые важные подробности об этом использовании.

  1. Когда символ встречается более одного раза в посл-обр, второе и последующие вхождения не приводят к повторному связыванию, а вместо этого разворачиваются в проверку на равенство с помощью eq.

    В следующем примере представлена форма pcase с двумя ветвями и двумя посл-обр, A и B. И A, и B сначала проверяют, что знач-выр является парой (с использованием pred), а затем связывают символы с car и cdr знач-выр (используя по одному app).

    Для A, поскольку символ st упоминается дважды, второе упоминание становится проверкой на равенство с помощью eq. С другой стороны, B использует два разных символа, s1 и s2, оба из которых становятся независимыми привязками.

    (defun grok (object)
      (pcase object
        ((and (pred consp)        ; посл-обр A
              (app car st)        ; первое упоминание: st
              (app cdr st))       ; второе упоминание: st
         (list 'eq st))
    
        ((and (pred consp)        ; посл-обр B
              (app car s1)        ; первое упоминание: s1
              (app cdr s2))       ; первое упоминание: s2
         (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. Код с побочными эффектами, ссылающийся на символ, не определён. Избегайте этого. Например, вот две похожие функции. Обе используют and, символ и guard:
    (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)  ; НЕВЕРНО!
    

    Разница в булево-выражении в guard: CLEAN ссылается на n просто и напрямую, а MAYBE ссылается на n с побочным эффектом в выражении (incf n). Когда integer равно 3, происходит следующее:

    • Первое n связывает его с знач-выр, т.е. с результатом вычисления (* 3 3), то есть 9.
    • булево-выражение вычисляется:
      начало:   (< 9 (incf n)        100)
      становится: (< 9 (setq n (1+ n)) 100)
      становится: (< 9 (setq n (1+ 9)) 100)
      
      становится: (< 9 (setq n 10)     100)
                                         ; побочный эффект здесь!
      становится: (< 9       n         100) ; n теперь связано с 10
      становится: (< 9      10         100)
      становится: t
      
    • Поскольку результат вычисления не равен nil, guard сопоставляется, and сопоставляется, и управление передаётся тело-формам этой ветви.

    Помимо математической некорректности утверждения, что 9 является двузначным целым, есть ещё одна проблема с MAYBE. Тело-форма ссылается на n ещё раз, но мы не видим обновлённое значение — 10 — вообще. Что с ним случилось?

    Резюмируя, лучше вообще избегать ссылок с побочными эффектами на образцы символ, не только в булево-выражении (в guard), но также в выр (в let) и функция (в pred и app).

  3. При сопоставлении тело-формы ветви могут ссылаться на набор символов, которые образец связывает с помощью let. Когда посл-обр — это and, этот набор является объединением всех символов, которые связывает каждый из его под-образцов. Это имеет смысл, потому что для сопоставления and все под-образцы должны сопоставиться.

    Когда посл-обр — это or, ситуация иная: or сопоставляется с первым под-образцом, который сопоставился; остальные под-образцы игнорируются. Не имеет смысла, чтобы каждый под-образец связывал с помощью let разные наборы символов, потому что тело-формы не имеют возможности различить, какой под-образец сопоставился, и выбрать один из разных наборов. Например, следующий код недопустим:

    (require 'cl-lib)
    (pcase (read-number "Введите целое число: ")
      ((or (and (pred cl-evenp)
                e-num)      ; связать e-num с знач-выр
           o-num)           ; связать o-num с знач-выр
       (list e-num o-num)))
    
    
    
    Введите целое число: 42
    ошибка→ Значение символа как переменной: void: o-num
    
    Введите целое число: 149
    ошибка→ Значение символа как переменной: void: e-num
    

    Вычисление тело-формы (list e-num o-num) вызывает ошибку. Чтобы различать под-образцы, можно использовать другой символ, одинаковый по имени во всех под-образцах, но различающийся по значению. Перерабатывая приведённый выше пример:

    (require 'cl-lib)
    (pcase (read-number "Введите целое число: ")
      ((and num                                ; строка 1
            (or (and (pred cl-evenp)           ; строка 2
                     (let spin 'even))         ; строка 3
                (let spin 'odd)))              ; строка 4
       (list spin num)))                       ; строка 5
    
    
    
    Введите целое число: 42
    ⇒ (even 42)
    
    Введите целое число: 149
    ⇒ (odd 149)
    

    Строка 1 «выносит» связывание знач-выр с помощью and и символ (в данном случае num). В строке 2 or начинается так же, как и раньше, но вместо связывания разных символов дважды использует let (строки 3–4) для связывания одного и того же символа spin в обоих под-образцах. Значение spin различает под-образцы. Тело-форма ссылается на оба символа (строка 5).