Программное построение функции Common Lisp с помощью defun

Я хотел бы определить функцию, используя defun, но не на верхнем уровне. (Имя и тело функции должны быть созданы на основе пользовательского ввода.) Нижеследующее иллюстрирует основную цель:

(let ((name 'fn1)
      (body "abc"))
  (eval `(defun ,name ()
           ,body))
  (push name *function-names*))

1) Это работает, но как это сделать без eval?

2) В настоящее время я компилирую ряд таких функций с помощью

(loop for fn-name in *function-names* do
      (setf (symbol-function fn-name)
        (compile nil (symbol-function fn-name))))

но есть ли лучший способ использовать только имя fn, например `(compile ,fn-name), чтобы избежать предупреждения компилятора SBCL, когда тело функции содержит рекурсивный вызов функции?


person davypough    schedule 08.05.2017    source источник
comment
вы пробовали (let ((name 'fn1) (body "abc")) (setf (symbol-function name) (compile nil `(lambda () ,body)))) или что-то в этом роде? clhs.lisp.se/Body/f_cmp.htm   -  person Will Ness    schedule 09.05.2017
comment
@Will Ness, Красиво и лаконично, но мне интересно, позаботится ли defun о некоторых тонкостях, которые symbol-function могут не учитывать (на основе комментариев Райнера Джосвига ниже). В моем приложении сконструированные функции определяются во время загрузки, так что, возможно, eval не так уж и плох.   -  person davypough    schedule 10.05.2017
comment
поэтому это был только комментарий. :)   -  person Will Ness    schedule 10.05.2017


Ответы (2)


Выполнение этого без DEFUN делает это немного проблематичным. Я думаю, что это часть ANSI Common Lisp, которая не так хороша.

Если вы посмотрите на реализации Common Lisp, вы увидите, что они расширяют форму DEFUN до конструкций, специфичных для реализации. Часто мы видим что-то вроде именованной лямбды.

СБКЛ:

(defun foo ()
  (foo))

->

(SB-INT:NAMED-LAMBDA FOO ()
  (BLOCK FOO (FOO)))

который оценивается как:

#<FUNCTION FOO {1003271FFB}>

К сожалению, именованная лямбда не является концепцией в ANSI CL.

Чтобы получить это, вам нужно:

  • построить форму DEFUN
  • ОЦЕНИТЕ это
  • опционально скомпилировать его

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

В качестве альтернативы мы могли бы попытаться не использовать DEFUN.

  • нам нужно убедиться, что глобальное имя не является макросом
  • затем мы строим лямбда-выражение
  • затем мы СОСТАВЛЯЕМ это выражение

Пример: рекурсивная функция:

(compile 'my-fn '(lambda () (my-fn))

Теперь мы все еще можем получить предупреждение о неопределенной функции при компиляции лямбды. Как мы можем указать имя? LABELS сделает это:

(compile 'my-fn
         (lambda ()
           (labels ((my-fn ()
                      (my-fn)))
             (my-fn))))

LABELS также настроит BLOCK, как и DEFUN.

Теперь, когда мы вызываем глобальный MY-FN, он вызывает локальный MY-FN, который затем может вызывать себя рекурсивно.

Посмотрим, есть ли еще альтернативные подходы...

  • Можно также использовать обычную LAMBDA с COMPILE и сначала объявить функцию. Это также может подавить предупреждение о компиляции.
person Rainer Joswig    schedule 09.05.2017

Я немного не понимаю, «почему» вы пытаетесь сделать это с помощью defun, но если вы хотите, чтобы пользователь мог определять свои собственные функции, а затем вызывать их, есть несколько способов сделать это.

1) если вы оцениваете его как обычную функцию defun, вы можете просто сделать что-то вроде

(loop for f in *my-functions-names* and
      for a in *my-functions-args* and
      for b in *my-functions-bodies*
  do (setf (symbol-function f) (compile `(lambda ,a ,b))))

тогда пользователи могли бы компилировать свои функции во время выполнения, и это выглядело бы так, как будто они были там все время.

2) создать хеш-таблицу лямбд и funcall (или apply) их функций:

(defparameter *user-functions* (make-hash-table))

(defun def-user-fun (f-name f-args f-body &optional (ns *user-functions*))
  (setf (gethash f-name ns) (compile nil `(lambda ,f-args ,f-body))))

(defun call-user-fun (f-name args &optional (ns *user-functions*))
  (funcall (gethash f-name *user-funcations) f-args))

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

3) если это просто для того, чтобы сэкономить ваше время при компиляции собственного кода, вы можете просто сделать то, что сделали, но с помощью defmacro и оператора цикла.

Обновить

Поскольку вам нужна рекурсия, вы можете сделать что-то похожее на то, что предложил @rainerjoswig с (lambda (args..) (labels ((recurse ...)) (recurse ...))). Хотя это может показаться нехорошим в императивном смысле, в Лиспе это идиоматично. Фактически, как компилятор SBCL, так и сборщик мусора настроены специально для такого рода рекурсии. Если вы хотите узнать об оптимизации Common Lisp, на которую вы можете положиться, прочтите стандарт.

(compile 'fname) == (setf (symbol-function 'fname) (compile nil (symbol-function 'fname)))? Да и нет. По крайней мере, вероятно, не так, как вы думаете. Мне кажется, что вы запутались в двух вещах, обе из которых являются частью загадочного механизма "бэкенда" Лиспа.

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

Во-вторых, compile также не работает таким образом. Вы можете либо передать ему имя symbol-function, с помощью которого он получает доступ к определению хранимой функции, компилирует его, а затем старое определение с скомпилированным определением, (compile 'my-func), вы можете передать ему выражение lambda, которое оно скомпилирует, (compile nil (lambda (args...) body)) или, наконец, вы можете передать как имя, так и определение, которое будет хранить скомпилированную функцию как эту функциональную переменную.

Так что да, в некотором смысле он расширяется до setf, но не до вашего конкретного setf, потому что вам не следует делать (compile nil (symbol-function 'fname)).

Что касается разрешения рекурсии, то достаточно просто расширить @rainerjoswig, разрушив общую форму defun, например

(def-user-func count-down (val) 
  (if (> val 0)
    (progn (print val) (count-down (1- val)))
    nil))

с таким макросом

(defmacro def-user-func (name args &body body)
  `(compile ',name (lambda (,@args)
                     (labels ((,name (,@args)
                                ,@body))
                        (,name ,@args)))))

Из-за того, что lisp скрывает имена переменных в пределах let, labels и других подобных утверждений, это будет работать отлично. «Внешний» мир просто видит функцию, в то время как функция видит только свою собственную функцию labels. Когда ,@body расширяется, это будет происходить в контексте labels, и поскольку мы продублировали имена, это будет работать правильно.

person Alex Hernandez    schedule 09.05.2017
comment
Да, я не включил много фона. Для этого приложения пользователь создает спецификационный файл, содержащий логические выражения предикатов (и другие данные). Я использую этот ввод для создания и компиляции функций, которые могут эффективно обращаться к внутренним структурам данных во время выполнения. (Пользователь может использовать имена, которые будут преобразованы в рекурсивные вызовы функций.) Таким образом, цель состоит в том, чтобы настроить все во время загрузки до получения окончательных инструкций по выполнению. - person davypough; 10.05.2017
comment
Я не эксперт, но теперь мне кажется, что eval с defun, а затем compile — самый простой подход. В чем я не уверен, так это в том, что (compile 'fname) == (setf (symbol-function 'fname) (compile nil (symbol-function 'fname)). - person davypough; 10.05.2017
comment
В то время как eval над defun будет работать, это также открывает возможность атаки путем инъекции или случайного отказа в обслуживании в вашей системе, поэтому, если вы все же пойдете по этому пути, будьте осторожны с тем, что им разрешено доступ. - person Alex Hernandez; 10.05.2017