【问题标题】:emulating Clojure-style callable objects in Common Lisp在 Common Lisp 中模拟 Clojure 风格的可调用对象
【发布时间】:2015-03-15 18:09:53
【问题描述】:

在 Clojure 中,hash-map 和向量实现了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 中函数对象上的 wiki entry,似乎拥有一个单独的函数命名空间会使 CL 的问题变得复杂,而在 Scheme 中,您可以使用闭包来做到这一点。

【问题讨论】:

    标签: clojure common-lisp lisp-2 callable-object


    【解决方案1】:

    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 通用函数也是类 STANDARD-GENERIC-FUNCTION 的 CLOS 实例,并且可以拥有和使用该类的用户定义子类。但这通常不会被利用。

    推荐

    因此,最好适应不同的语言风格并按原样使用 CL。在这种情况下,Common Lisp 不够灵活,无法轻松合并这样的功能。一般的 CL 风格是不省略用于次要代码优化的符号。危险在于混淆和只写代码,因为很多信息不是直接在源代码中的话。

    【讨论】:

    • 实际上,在 Clojure 中,这超出了命名空间分离的范围。正如您所期望的那样,以下在 Clojure 中的工作:({:a "first key" :b "second key"} :a) 将产生 "first key",因此 Clojure 在这方面的行为类似于 Lisp Machine Lisp。所以,是的,在 Clojure 中,对于某些类型,“数据在函数命名空间中”。 “我们想要那个吗?”显然,korrok 对此的回答是肯定的,但 Common Lisps 的回答非常否定。 JFTR,虽然 Clojure 的实现方式提供了一些非常简洁的解决方案,但它们有时难以掌握。
    • @schauerho:问题是,考虑到它的语义,我们是否希望在 Common Lisp 中使用它? CL 是 Lisp-2,Clojure 是 Lisp-1。
    • 我也希望文字数据作为调用中的函数没有那么有用,因为它相对较少。使用绑定数据作为函数似乎是一个不太罕见的用例......
    • 不,我同意我们不这样做。我试图暗示我也更喜欢 Lisp-2,尽管我能理解为什么有些人的意见不同。
    • Fset 提供了@函数,它要么执行向量/表查找,要么根据给定的内容调用函数。这似乎是 lisp-2 的等价物。我认为可调用表仍然有一个好处,特别是在 lisp-2 中:您可以将它们传递给地图和朋友。
    【解决方案2】:

    虽然可能无法完全按照您的意愿进行操作,但有一些方法可以将类似的东西组合在一起。一种选择是定义一个新的绑定形式,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。


    另一个类似于可调用对象的选项是访问ssyntax 的Arc 结构。基本上 x.5 访问 x 中索引 5 处的元素。我能够在 Common Lisp 中实现这一点。你可以看到我为它写的代码herehere。我也对其进行了测试,因此您可以看到使用它的样子here

    我的实现是如何工作的,我编写了一个带有 ssyntax 的宏,它查看正文中的所有符号并为其中一些定义宏和符号宏。例如,x.5 的符号宏是 (get x 5),其中 get 是我定义的访问结构的通用函数。这样做的缺陷是我总是必须在任何我想使用 ssyntax 的地方使用 w/ssyntax。幸运的是,我能够将它隐藏在一个类似于 defun 的宏 def 中。

    【讨论】:

    • 可能的。但是(v n)(aref v n) 慢十到二十倍,因为调用开销,消耗内存,...
    • @RainerJoswig 我猜你可以有 with-callable generate marcolet 和 symbol-macrolet 绑定,使可调用对象看起来像函数。本地宏将在编译时扩展,使 (v n) 等效于 (aref v n) 并且符号宏仍将允许将可调用对象传递给其他函数 [即(mapcar v '(0 1 2)) 将获得前三个元素]。它会更复杂,但不会复杂很多。
    • 您通常不会知道编译时对象的类型,除非您可以访问编译时类型信息(查看它是 AREF 还是 GETHASH 或其他别的...)。因此编译器不会知道某物是一个简单的向量,并将访问编译成一个内联的简单向量引用......
    • 我的错。要么必须明确提供该信息(如果您要使用 aref,您已经知道它是一个数组,您只需将其告知 with-callable),或者将其扩展为通用访问函数。
    【解决方案3】:

    我同意 Rainer Joswig 的建议:最好适应 Common Lisp 的做事方式——就像在切换到 Clojure 时,让 Common Lisp 程序员适应 Clojure 的做事方式更好一样。但是,正如 malisper 的复杂答案所示,可以做你想做的一部分。这是一个更简单策略的开始:

    (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 的函数单元格,setfmake-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 的打印程​​序做一些花哨的事情。也可以允许设置数组的值,但这可能需要单独的符号。

    【讨论】:

      猜你喜欢
      • 2015-06-04
      • 2015-05-10
      • 2020-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多