【问题标题】:Passing Variable Bindings to Functions将变量绑定传递给函数
【发布时间】:2019-03-11 10:24:52
【问题描述】:

我有下面的 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

因此,我认为我需要一些“绑定”宏,我可以在其中传递“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 包装,但这会带走我也选择实现这个接口的原因,即避免嵌套,这取决于我喜欢阅读的测试在第二个界面。所以这就是我提出这个问题的原因,真正的问题是如何在不使用显式 let 的情况下将上下文从 suite-1 传递到 test-1 和 test-2。

【问题讨论】:

    标签: binding common-lisp let


    【解决方案1】:

    没有办法用词法变量来做到这一点。根据您的用例,您可以尝试以下几种选择:

    1. 首选的解决方案是重新考虑问题并找到一种方法,以便在定义函数时使绑定可用,或者将值作为参数传递。

    2. 改为使用特殊绑定。问题当然是绑定将在整个动态范围内可见,但在某些情况下可能没问题。例如,MOP 有一个很少使用的可调用实例功能,您可以使用它来定义一种函数类型,将特殊绑定保存在哈希表中。

      (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 来编译带有绑定的 lambda 表达式。在普通的应用程序代码中,这可能是一个坏主意,但对于测试框架来说,它可能没问题(您可能希望框架为宏、编译器宏和内联函数编译测试用例以使用当前定义)。

    更新后

    根据更新,您正在寻找选项 3。您可以将绑定与套件一起存储,并且在运行测试时,使用COMPILE 编译一个注入绑定的 lambda 表达式。一个非常简单的例子(忽略与问题无关的真实测试框架的所有复杂性):

    (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
    

    【讨论】:

    • 我正在这里尝试,我无法得到您在第三个解决方案中所说的内容。
    • 我刚刚添加了一个例子。
    • 其实我想用套件 1 提供 (x 0) 来实现,如果 test-1 将 x 更改为 1,则 test-1 和 test-2 可以访问它 x will 为 1,必要时会使用钩子来避免它发生:before-all、after-all、before-each 和 after-each。但是根据你给我的信息,我认为它会起作用,我会在这里尝试。
    • 我添加了一个包含所有测试的单个动态绑定的示例。
    猜你喜欢
    • 2020-12-04
    • 2011-05-17
    • 1970-01-01
    • 2020-07-28
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    • 1970-01-01
    相关资源
    最近更新 更多