Эмулировать вызываемые объекты в стиле 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. Также возможно разрешить установку значений массива, но для этого, вероятно, потребуются отдельные символы.