- PVSM.RU - https://www.pvsm.ru -

«Lambda over matching» макросы в Common Lisp

Рассмотрим примеры:

(mapcar (lambda (x)
          (case x
            (1 :one)
            (2 :two)
            (3 :three)))
        (list 0 1 2 3 4 5))

(mapcar (lambda (x)
          (typecase x
            (number :number)
            (string :string)
            (symbol x)))
        (list :foo 1 :bar 2 "baz" 3))

Видно, что (lambda (x) (case x ...)) и (lambda (x) (typecase x ...) — шаблонный код. Попробуем избавиться от него.

Подобный boilerplate давно известен. Например, в OCaml есть конструкция совмещающая сопоставление с шаблоном (match expr with ...) с записью λ функции.
Например, такой код:

List.map (function x -> match x with
                        | 1 -> "one"
                        | 2 -> "two"
                        | _ -> "otherwise") [0; 1; 2; 3; 4; 5]

совершенно свободно заменяется на:

List.map (function 1 -> "one"
                 | 2 -> "two"
                 | _ -> "otherwise") [0; 1; 2; 3; 4; 5]

В библиотеке optima [1], которая реализует pattern matching как в функциональных языках, в пакете optima.extra имеются макросы lambda-match [2], lambda-ematch [3], lambda-cmatch [4] которые именно эту проблему шаблонного кода и решают.
Но для конструкций сопоставления, таких как {c,e}case [5], {c,e}typecase [6] и {c,e}switch [7] из alexandria [8] подобных макросов не предусмотрено. Исправим это. Пусть подобного рода макросы называются «lambda over matching» макросами, для идентификации.
«lambda over case»:

(defmacro lambda-case (&body clauses)
  (with-gensyms (keyform)
    `(lambda (,keyform)
       (case ,keyform
         ,@clauses))))

С «lambda over typecase» макросом не всё так просто. В большинстве случаев понадобится доступ к сопоставляемому значению (x, в изначальном примере). В OCaml есть возможность связывать переменные с сопоставленными фрагментами или со всей сопоставляемой структурой (частый случай), например:

List.map (function
           | (1 | 2) as x -> x + 1
           |  3           -> 30
           |  _           -> 99) [0; 1; 2; 3; 4; 5]

Есть варианты: можно «пробросить» it как в классических анафорических макросах или, например, явно задать связывание, как в макросах навроде named-lambda [7]. Я остановлюсь на первом варианте:

(defmacro lambda-typecase (&body clauses)
  `(lambda (it)
     (typecase it
       ,@clauses)))

Теперь изначальные примеры будут выглядеть вот так:

(mapcar (lambda-case
          (1 :one)
          (2 :two)
          (3 :three))
        (list 0 1 2 3 4 5))

(mapcar (lambda-typecase
          (number :number)
          (string :string)
          (symbol it))
        (list :foo 1 :bar 2 "baz" 3))

Сейчас как в OCaml за тем лишь исключением, что в OCaml этот синтаксический сахар «прибит гвоздями» к языку, а в CL расширен макросами и, естественно, такой подход применим к любым конструкциям сопоставления.

Конечно, для симметрии, имеет смысл определить lambda-ccase, lambda-ecase и lambda-ctypecase, lambda-etypecase. А ещё придумать, как написать lambda-switch макрос, а конкретнее, как «пробросить» предикат и функцию-трансформатор (test и key аргументы) в switch.


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/pesochnitsa/72589

Ссылки в тексте:

[1] optima: https://github.com/m2ym/optima

[2] lambda-match: https://github.com/m2ym/optima#macro-lambda-match

[3] lambda-ematch: https://github.com/m2ym/optima#macro-lambda-ematch

[4] lambda-cmatch: https://github.com/m2ym/optima#macro-lambda-cmatch

[5] {c,e}case: http://www.lispworks.com/documentation/lw445/CLHS/Body/m_case_.htm#case

[6] {c,e}typecase: http://www.lispworks.com/documentation/lw445/CLHS/Body/m_tpcase.htm#ctypecase

[7] {c,e}switch: http://common-lisp.net/project/alexandria/draft/alexandria.html#Data-and-Control-Flow

[8] alexandria: http://common-lisp.net/project/alexandria/draft/alexandria.html