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

в 18:12, , рубрики: Песочница, метки: , , ,

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

(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, которая реализует pattern matching как в функциональных языках, в пакете optima.extra имеются макросы lambda-match, lambda-ematch, lambda-cmatch которые именно эту проблему шаблонного кода и решают.
Но для конструкций сопоставления, таких как {c,e}case, {c,e}typecase и {c,e}switch из alexandria подобных макросов не предусмотрено. Исправим это. Пусть подобного рода макросы называются «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. Я остановлюсь на первом варианте:

(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.


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js