Эмулировать вызываемые объекты в стиле Clojure в Common Lisp

В Clojure реализованы хеш-карты и векторы invoke, так что они могут быть использованы в качестве функций, например,

(let [dict {:species "Ursus horribilis"
            :ornery :true
            :diet "You"}]
  (dict :diet))

lein> "You"

или, для векторов,

(let [v [42 613 28]]
  (v 1))

lein> 613

Можно создавать вызываемые объекты в Clojure, если они реализуют IFn, Я новичок в Common Lisp - возможны ли вызываемые объекты, и если да, то что будет включать в себя реализацию? Я бы очень хотел иметь возможность делать такие вещи, как

(let ((A (make-array (list n n) ...)))
   (loop for i from 0 to n
         for j from 0 to m
      do (setf (A i j) (something i j)))
   A)

вместо того, чтобы код был завален aref, Точно так же было бы здорово, если бы вы могли получить доступ к записям других структур данных, например, к словарям, таким же образом.

Я посмотрел на вики-запись об объектах функций в Lisp/Scheme, и кажется, что наличие отдельного пространства имен функций усложнит дело для CL, тогда как в Scheme вы можете просто сделать это с замыканиями.

3 ответа

Решение

Пример вызываемых объектов в предшественнике Common Lisp

Вызываемые объекты были предоставлены ранее. Например, в Lisp Machine Lisp:

Command: ("abc" 1)            ; doesn't work in Common Lisp
#\b

Привязки в Common Lisp

Common Lisp имеет отдельные пространства имен для функций и значений. Так (array 10 1 20) имеет смысл только тогда, когда array будет символом, обозначающим функцию в пространстве имен функции. Таким образом, значение функции тогда будет вызываемым массивом.

Привязка значений к переменным, действующим как функции, в основном противоречит назначению различных пространств имен для функций и значений.

(let ((v #(1 2 3)))          
  (v 10))                    ; doesn't work in Common Lisp

Выше не имеет смысла в языке с различными пространствами имен для функций и значений.

FLET используется для функций вместо LET,

(flet ((v #(1 2 3 4 5 6 7))) ; doesn't work in Common Lisp
  (v 4))                     

Это будет означать, что мы поместим данные в пространство имен функции. Мы хотим этого? На самом деле, нет.

Литеральные данные как функции в вызовах функций.

Можно также подумать о том, чтобы по крайней мере позволить буквальным данным действовать как функции при непосредственном вызове функции:

(#(1 2 3 4 5 6 7) 4)         ; doesn't work in Common Lisp

вместо

(aref #(1 2 3 4 5 6 7) 4)

Common Lisp не допускает этого каким-либо тривиальным или относительно простым способом.

Примечание:

Можно реализовать что-то в направлении интеграции функций и значений с CLOS, поскольку универсальные функции CLOS также являются экземплярами класса CLOS. STANDARD-GENERIC-FUNCTION и возможно иметь и использовать пользовательские подклассы этого. Но это обычно не эксплуатируется.

Рекомендация

Поэтому лучше всего приспособиться к другому языковому стилю и использовать CL как есть. В этом случае Common Lisp недостаточно гибок, чтобы легко включать такую ​​функцию. Это общий стиль CL, который не пропускает символы для незначительных оптимизаций кода. Опасность заключается в запутывании и коде только для записи, потому что большая часть информации не находится непосредственно в исходном коде.

Хотя, возможно, не существует способа сделать именно то, что вы хотите, есть несколько способов соединить что-то подобное. Одним из вариантов является определение новой формы привязки with-callable, которая позволяет нам связывать функции локально с вызываемыми объектами. Например, мы могли бы сделать

(with-callable ((x (make-array ...)))
  (x ...))

примерно эквивалентно

(let ((x (make-array ...)))
  (aref x ...))

Вот возможное определение для with-callable:

(defmacro with-callable (bindings &body body)
  "For each binding that contains a name and an expression, bind the
   name to a local function which will be a callable form of the
   value of the expression."
  (let ((gensyms (loop for b in bindings collect (gensym))))
    `(let ,(loop for (var val) in bindings
                 for g in gensyms
                 collect `(,g (make-callable ,val)))
       (flet ,(loop for (var val) in bindings
                    for g in gensyms
                    collect `(,var (&rest args) (apply ,g args)))
         ,@body))))

Осталось только определить различные методы для make-callable, которые возвращают замыкания для доступа к объектам. Например, вот метод, который определил бы его для массивов:

(defmethod make-callable ((obj array))
  "Make an array callable."
  (lambda (&rest indices)
    (apply #'aref obj indices)))

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

(defmacro defcallable (type args &body body)
  "Define how a callable form of TYPE should get access into it."
  `(defmethod make-callable ((,(car args) ,type))
     ,(format nil "Make a ~A callable." type)
     (lambda ,(cdr args) ,@body)))

Теперь, чтобы сделать вызываемые массивы, мы будем использовать:

(defcallable array (obj &rest indicies)
  (apply #'aref obj indicies))

Намного лучше. Теперь у нас есть форма with-callable, которая будет определять локальные функции, которые позволяют нам получать доступ к объектам, и макрос defcallable, который позволяет нам определять, как создавать вызываемые версии других типов. Одним из недостатков этой стратегии является то, что мы должны явно использовать with-callable каждый раз, когда мы хотим сделать объект вызываемым.


Другая опция, которая похожа на вызываемые объекты, - это структура Arc, обращающаяся к ssyntax. Обычно х.5 обращается к элементу с индексом пять в х. Я смог реализовать это в Common Lisp. Вы можете увидеть код, который я написал для этого здесь и здесь. У меня также есть тесты для этого, чтобы вы могли увидеть, как это выглядит здесь.

Как работает моя реализация, я написал макрос с ssyntax, который просматривает все символы в теле и определяет макросы и символьные макросы для некоторых из них. Например, символ-макрос для x.5 будет (get x 5), где get - это определенная мной общая функция, которая обращается к структурам. Недостаток в том, что я всегда должен использовать w/ssyntax везде, где я хочу использовать ssyntax. К счастью, я могу спрятать его в макросе def, который действует как defun.

Я согласен с советом Райнера Джосвига: было бы лучше освоиться с обычным образом работы Common Lisp - так же, как программисту Common Lisp лучше было бы освоиться с образом работы Clojure при переходе на Clojure. Однако, как показывает сложный ответ Малиспера, можно делать часть того, что вы хотите. Вот начало более простой стратегии:

(defun make-array-fn (a) 
  "Return a function that, when passed an integer i, will 
  return the element of array a at index i."
  (lambda (i) (aref a i)))

(setf (symbol-function 'foo) (make-array-fn #(4 5 6)))

(foo 0) ; => 4
(foo 1) ; => 5
(foo 2) ; => 6

symbol-function получает доступ к функциональной ячейке символа foo, а также setf помещает объект функции, созданный make-array-fn внутрь. Так как эта функция находится в ячейке функции, foo может использоваться в функциональной позиции списка. Если вы хотите, вы можете заключить всю операцию в макрос, например, так:

(defmacro def-array-fn (sym a)
  "Define sym as a function that is the result of (make-array-fn a)."
  `(setf (symbol-function ',sym)
         (make-array-fn ,a)))

(def-array-fn bar #(10 20 30 40))

(bar 0) ; => 10
(bar 1) ; => 20
(bar 3) ; => 40

Конечно, "массив", определенный таким образом, больше не выглядит как массив. Я полагаю, вы могли бы сделать что-то необычное с процедурами печати CL. Также возможно разрешить установку значений массива, но для этого, вероятно, потребуются отдельные символы.

Другие вопросы по тегам