Передача привязок переменных в функции - PullRequest
0 голосов
/ 11 марта 2019

У меня есть код Javascript ниже.Что было бы эквивалентно в Common Lisp?

function A () {
}
function B () {
}

var a1 = new A();
var b1 = new B();

a1.done.bind(b1);

Что я пытаюсь сделать?Мне нужно передать контекст (то, что я имею в виду в контексте - это то, что создает let, привязки переменных) для функций, которые будут выполняться.

У меня есть функции x1 и x2, я хочу, чтобы онииметь доступ к переменным let.Проблема в том, что мне нужно передать функцию как переменную.См. Мою попытку ниже:

(defmacro create-context (vars &body body)
  `(let ,vars
     ,@body))

(create-context ((x 2) (y 3)) (+ x y))

(defmacro create-suite-context (vars fn)
  (with-gensyms (childs)
    `(let ((,childs '()))
       (create-context
           ,vars
         (push ,fn ,childs)))))

(let* ((a (create-suite-context ((x 2)) (lambda () (+ x 1)))))
  (funcall (car a)))
;; return 3 - OK

(let* ((f (lambda () (+ x 1)))
       (a (create-suite-context ((x 2)) f)))
  (funcall (car a)))
;; The variable X is unbound.

Я понимаю, почему x не найден, это происходит из-за этого:

(let ((f (lambda () (+ x 1))))
  (macroexpand-1 '(create-suite-context
                   ((x 2))
                   f)))
; in: LET ((F (LAMBDA () (+ X 1))))
;     (LET ((F (LAMBDA () (+ X 1))))
;       (MACROEXPAND-1 '(CREATE-SUITE-CONTEXT ((X 2)) F)))
; 
; caught STYLE-WARNING:
;   The variable F is defined but never used.
; in: LET ((F (LAMBDA () (+ X 1))))
;     (+ X 1)
; 
; caught WARNING:
;   undefined variable: X
; 
; compilation unit finished
;   Undefined variable:
;     X
;   caught 1 WARNING condition
;   caught 1 STYLE-WARNING condition
(LET ((#:G591 'NIL))
  (CREATE-CONTEXT ((X 2))
    (PUSH F #:G591)))
T

Что отличается от этого:

(macroexpand-1 '(create-suite-context
                 ((x 2))
                 (lambda () (+ x 1))))
(LET ((#:G592 'NIL))
  (CREATE-CONTEXT ((X 2))
    (PUSH (LAMBDA () (+ X 1)) #:G592)))
T

Поэтому я думаю, что мне понадобится некоторый макрос «bind», в котором я мог бы передать переменные «vars», чтобы функции имели доступ.

Примечание: я знаю, что нетнужен макрос create-context, потому что то, что он делает let, уже делает, но он должен был лучше объяснить, что я имею в виду под контекстом.

вверх после ответа @jkiiski

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

(set-ui-cacau 'new-tdd)
(suite :suite-1
       (let ((x y z))
         (test :test-1
               (let ((actual nil))
                 (t-p t))
               :timeout 50)

         (test :test-2
               (let ((actual nil))
                 (t-p t))
               :timeout 70)))
(run-cacau :reporter 'min)

;; or

(set-ui-cacau 'classic)
(in-suite :suite-1
          :timeout 30
          :parent :root)

(test :test-1
      (let ((actual nil))
        (t-p actual))
      :timeout 50)

(test :test-2
      (let ((actual nil)
            (expected 1))
        (setf actual 1)
        (eq-p actual expected))
      :timeout 70)
(run-cacau :reporter 'min)

Как вы можете видеть, с первым интерфейсом легче работать, потому что у меня есть доступныепривязок.Во втором интерфейсе нет способа сделать это, я мог бы также обернуть его в let, но это убрало бы причину, по которой я также решил реализовать этот интерфейс, то есть избежать вложенности, в зависимости от тестов, которые я предпочитаю читатьна втором интерфейсе.Вот почему я задал этот вопрос, где реальная проблема заключается в том, как я мог бы передать контекст из suite-1 в test-1 и test-2 без использования явного let.

1 Ответ

4 голосов
/ 11 марта 2019

Нет способа сделать это с лексическими переменными. В зависимости от вашего варианта использования, есть несколько вариантов, которые вы можете попробовать:

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

  2. Вместо этого используйте специальные привязки. Проблема, конечно, в том, что привязки будут видны во всей динамической области, но в некоторых случаях это может быть нормально. Например, MOP имеет редко используемую особенность funcallable экземпляров, которую вы можете использовать для определения типа функции, хранящей специальные привязки в хеш-таблице.

    (ql:quickload :closer-mop)
    
    (defclass context-fn (closer-mop:funcallable-standard-object)
      ((context :initform (make-hash-table)
                :accessor context-fn-context))
      (:metaclass closer-mop:funcallable-standard-class))
    
    (defmethod initialize-instance :after ((context-fn context-fn)
                                           &key (fn (error "Must have a :FN")))
      (closer-mop:set-funcallable-instance-function
       context-fn
       (lambda (&rest args)
         (let (vars vals)
           (maphash (lambda (key value)
                      (push key   vars)
                      (push value vals))
                    (context-fn-context context-fn))
           (progv vars vals
             (apply fn args))))))
    
    (defun context-bind (fn name value)
      (check-type fn context-fn)
      (check-type name symbol)
      (setf (gethash name (context-fn-context fn)) value))
    
    (defmacro bind (fn name value)
      `(context-bind ,fn ',name ,value))
    
    (defmacro clambda (lambda-list &body body)
      `(make-instance 'context-fn :fn (lambda ,lambda-list ,@body)))
    
    (let ((fn (clambda (y)
                (declare (special x))
                (+ x y))))
      (bind fn x 100)
      (funcall fn 10))
    ;;=> 110
    
  3. В некоторых ситуациях вы можете составить список и использовать COMPILE для компиляции лямбда-выражения с привязками. В обычном коде приложения это, вероятно, будет плохой идеей, но, например, для инфраструктуры тестирования это может быть нормально (вы можете захотеть, чтобы среда все равно компилировала тестовые случаи для макросов, макросов компилятора и встроенных функций для использования текущего определения).

После обновления

Судя по обновлению, кажется, что вам нужен вариант 3. Вы можете хранить привязки в комплекте, а при запуске тестов используйте COMPILE, чтобы скомпилировать лямбда-выражение с введенными в него привязками. Очень простой пример (игнорируя все сложности реальной среды тестирования, которые не имеют отношения к вопросу):

(defstruct suite
  name
  bindings
  (tests (make-hash-table)))

(defvar *known-suites* (make-hash-table))
(defvar *suite*)

(defmacro suite (name &key bindings)
  `(setf (gethash ',name *known-suites*)
         (make-suite :name ',name :bindings ',bindings)))

(defmacro in-suite (name)
  `(setf *suite* (gethash ',name *known-suites*)))

(defmacro test (name form)
  `(setf (gethash ',name (suite-tests *suite*))
         ',form))

(defun run (&optional (suite *suite*))
  (let ((bindings (suite-bindings suite)))
    (format t "~s~%" (suite-name suite))
    (maphash (lambda (name form)
               (format t "~&~10<~:[Fail~;Success~]~> | ~s~%"
                       (funcall
                        (compile nil
                                 `(lambda ()
                                    (let ,bindings
                                      (declare (ignorable ,@(mapcar #'first bindings)))
                                      ,form))))
                       name))
             (suite-tests suite))))

(suite my-suite
       :bindings ((x 10)
                  (y 20)))

(in-suite my-suite)

(test my-test-1 (= x 15))

(test my-test-2 (evenp (+ x y)))

(run)
;; MY-SUITE
;;       Fail | MY-TEST-1
;;    Success | MY-TEST-2

Предполагается, что, хотя все тесты имеют эквивалентные привязки, привязки не обязательно должны быть одинаковыми, т. Е. (setf x 100) в одном тесте не должно быть видимым для других тестов, которые выполняются после него.

Вы можете изменить RUN, чтобы установить динамическое связывание всех тестов с помощью PROGV, чтобы сделать изменения видимыми для следующих тестов. Теперь переменные, конечно, стали специальными, а не лексическими.

(defun run/special (&optional (suite *suite*))
  (let ((bindings (suite-bindings suite)))
    (format t "~s~%" (suite-name suite))
    (progv
        (mapcar #'first bindings)
        (mapcar #'second bindings)
      (maphash (lambda (name form)
                 (format t "~&~10<~:[Fail~;Success~]~> | ~s~%"
                         (funcall
                          (compile nil
                                   `(lambda ()
                                      (declare (special ,@(mapcar #'first bindings)))
                                      ,form)))
                         name))
               (suite-tests suite)))))

(suite my-suite-2
       :bindings ((x 10)
                  (y 20)))

(in-suite my-suite-2)

(test my-test-3 (progn (incf x 5)
                       (= x 15)))

(test my-test-4 (evenp (+ x y)))

(run/special)
;; MY-SUITE-2
;;    Success | MY-TEST-3
;;       Fail | MY-TEST-4
...