【问题标题】:CLOS for Clojure?Clojure 的 CLOS?
【发布时间】:2011-05-02 01:47:24
【问题描述】:

Clojure 是否存在类似CLOS(通用 Lisp 对象系统)的东西?

【问题讨论】:

    标签: oop clojure lisp common-lisp clos


    【解决方案1】:

    您是否考虑过 Clojure 的 data types(尤其是 defrecord)、protocolsmultimethods?这三个在 Clojure 中总是比在这些机制之上的 CLOS 端口更惯用。

    【讨论】:

    • 再一次,如果我们都听过有人说“不要这样做,这很愚蠢”,史蒂夫·拉塞尔可能听了约翰·麦卡锡的意见,而从未手工编译过他的eval,我们可能从来没有在计算机上安装过 Lisp。从此开始了 Lisp 黑客的漫长历史,他们不尊重别人所说的正确做法!所以我说,去吧,将 CLOS 移植到 Clojure,看看会发生什么。真的,可能发生的最坏情况是什么? :-)
    • 我不是说“不要重新实现 CLOS”。我只是说,在尝试像对待牛一样对待马之前,至少应该考虑语言核心提供和支持的设施。现在,如果一个人勇敢(/愚蠢?),各种对象系统都可以建立在这些核心设施之上:如果一个人如此有动力,当然是 CLOS,而且可能还有更高级的路径。
    • @cemerick 默认数据类型缺乏继承。 isa+multimethods 是最接近的一种,但您需要自己继承字段。
    • CLOS 调度机制听起来真的很复杂,而且有点矫枉过正。 Clojure 协议没有走那么远,多方法似乎无法访问所有需要的信息。
    【解决方案2】:

    Clojure 没有 CLOS,也不需要 CLOS,但您可以实现它。

    Clojure 想要是不可变的,所以拥有可变的 OO 有点愚蠢,但你可以拥有一种 OO。

    有了这三样东西,你应该可以满足你的所有需求,但大多数时候,最好只使用普通函数和标准数据结构。

    【讨论】:

    • 我相信每一种语言都应该有初学者模式和专家模式。在初学者模式下,可以排除像OO这样的“危险”功能,但应该允许专家在有限的情况下使用这些功能(由专家决定)。例如,Java 不支持 C 风格的宏,因为 James Gosling 等人认为它们很危险。尽管有变通办法,但专家有时会诅咒他们的缺席。
    【解决方案3】:

    Clojure 本身没有对象系统,原因有两个:

    1. Clojure 专门设计用于托管在面向对象的平台上,然后它简单地吸收了底层平台的对象系统。 IE。 ClojureJVM 有 JVM 对象系统,ClojureCLR 有 CLI 对象系统,ClojureScript 有 ECMAScript 对象系统,等等。
    2. Rich Hickey 讨厌物体。

    但是,您可以显然在 Clojure 中实现一个对象系统。毕竟,Clojure 是图灵完备的。

    Mikel Evins 正在研究一种新的面向对象的方法,他称之为Categories。他有几个 Lisps 的实现,包括 Clojure(尽管并非所有端口都保证始终是最新的)。

    Categories 正慢慢被 Bard 所包含,这是 Mikel 正在设计的一种新的 Lisp 方言,它内置了 Categories。(然后,这可能会成为 Closos 的实现语言,这是 Mikel 的想法如何设计操作系统。)

    【讨论】:

    • “Rich Hickey 讨厌物体。”据我了解,Lisp 家族的“美”之一是程序员可以将语言扩展到她想要的任何东西,即使原作者不同意(或不同意:-))。我感谢 Rich 的出色语言设计,但我敢打赌,他会同意允许用户添加完整的对象系统是 Clojure(或任何 Lisp)的优势之一。
    【解决方案4】:

    这是一个旧帖子,但我想回复它。

    没有 clojure 没有 OO 支持,也没有 CLOS 支持。环境的底层对象系统仅在互操作性方面几乎不可用,而不是用于在 clojure 中创建自己的类/对象层次结构。 Clojure 旨在轻松访问 CLR 或 JVM 库,但 OOP 支持到此结束。

    Clojure 是一个 lisp 并支持闭包和宏。考虑到这 2 个特性,您可以用几行代码开发一个基本的对象系统。

    现在的重点是你真的需要 lisp 方言中的 OOP 吗?我会说不,是的。不,因为大多数问题都可以在没有对象系统的情况下解决,并且在任何 lisp 中都可以更优雅地解决。我会说是的,因为您仍然会不时需要 OOP,因此最好提供一个标准的参考实现,而不是让每个极客都实现它。

    我建议您看看 Paul Graham 的 On Lisp 书。您可以在线免费查阅。

    这真是一本好书,真正掌握了lisp的精髓。您必须稍微调整语法以适应 clojure,但概念保持不变。对您的问题很重要,最后一章中的一章展示了如何在 lisp 中定义自己的对象系统。

    顺便说一句,clojure 包含不变性。你可以在 clojure 中创建一个可变的对象系统,但是如果你坚持不变性,你设计,即使使用 OOP 也会有很大的不同。大多数标准设计模式和构造都考虑到了可变性。

    【讨论】:

      【解决方案5】:

      使用 OO 范式非常适合编写松散耦合的代码、模拟和测试。 Clojure 让这一切变得如此容易。

      我过去遇到的一个问题是代码依赖于其他代码。如果使用不当,Clojure 命名空间实际上会加剧问题。理想情况下,可以模拟命名空间,但正如我发现的那样......模拟命名空间存在很多问题:

      https://groups.google.com/forum/?fromgroups=#!topic/clojure/q3PazKoRlKU

      一旦您开始构建越来越大的应用程序,命名空间就会开始相互依赖,并且在没有大量依赖项的情况下单独测试您的更高级别的组件会变得非常不合适。大多数解决方案都涉及函数重新绑定和其他黑魔法,但问题是到了测试时间,原始依赖项仍在加载 -> 如果您有一个大型应用程序,这将成为大问题。


      在使用数据库库后,我有动力去寻找替代方案。数据库库给我带来了很多痛苦——它们需要很长时间才能加载,而且通常是应用程序的核心。如果不将整个数据库、库和相关外围设备带入您的测试代码,就很难测试您的应用程序。

      您希望能够打包您的文件,以便可以“换出”依赖于您的数据库代码的系统部分。 OO 设计方法提供了答案。

      很抱歉,答案很长...我想就为什么使用OO设计而不是如何使用它提供一个很好的理由。所以必须使用一个实际的例子。我已尝试保留 ns 声明,以便示例应用程序的结构尽可能清晰。

      现有的 clojure 样式代码

      本例使用carmine,这是一个redis客户端。与 korma 和 datomic 相比,它使用起来相对容易并且启动速度很快,但是数据库库仍然是数据库库:

      (ns redis-ex.history
        (:require [taoensso.carmine :as car]
                  [clojure.string :as st]))
      
      (defmacro wcr [store kdir f & args]
        `(car/with-conn (:pool ~store) (:conn ~store)
           (~f (st/join "/" (concat [(:ns ~store)] ~kdir)) ~@args)))
      
      (defn empty [store kdir]
        (wcr store kdir car/del))
      
      (defn add-instance [store kdir dt data]
         (wcr store kdir car/zadd dt data))
      
      (defn get-interval [store kdir dt0 dt1]
        (wcr store kdir car/zrangebyscore dt0 dt1))
      
      (defn get-last [store kdir number]
        (wcr store kdir car/zrange (- number) -1))
      
      (defn make-store [pool conn ns]
      {:pool pool
       :conn conn
       :ns ns})
      

      现有测试代码

      所有的功能都应该被测试...这不是什么新鲜事,是标准的clojure代码

      (ns redis-ex.test-history0
         (:require [taoensso.carmine :as car]
                   [redis-ex.history :as hist]))
      
      (def store
        (hist/make-store
         (car/make-conn-pool)
         (car/make-conn-spec)
         "test"))
      
      (hist/add-instance store ["hello"] 100 100) ;;=> 1
      (hist/get-interval store ["hello"] 0 200) ;;=> [100]
      

      面向对象的调度机制

      在观看 Misko Hevery 的演讲后,我想到了“OO”并不邪恶但实际上非常有用的想法:

      http://www.youtube.com/watch?v=XcT4yYu_TTs

      基本思想是,如果您想构建一个大型应用程序,您必须将“功能”(程序的核心)与“接线”(接口和依赖项)分开。依赖越少越好。

      我使用 clojure 哈希映射作为“对象”,因为它们没有库依赖关系并且是完全通用的(请参阅 Brian Marick 谈论在 ruby​​ 中使用相同的范例 - http://vimeo.com/34522837)。

      要使您的 clojure 代码“面向对象”,您需要以下函数 - (send 从 smalltalk 窃取) 如果它与现有键相关联,它只会调度与映射中的键相关联的函数。

      (defn call-if-not-nil [f & vs] 
         (if-not (nil? f) (apply f vs))
      
      (defn send [obj kw & args] 
         (call-if-not-nil (obj kw) obj))
      

      我在通用实用程序库中提供了实现(hara.fn 命名空间中的 https://github.com/zcaudate/hara)。如果您想自己实现,只需 4 行代码。

      定义对象“构造函数”

      您现在可以修改原来的make-store 函数以在地图中添加函数。现在您有了一定程度的间接性。

      ;;; in the redis-ex.history namespace, make change `make-store`
      ;;; to add our tested function definitions as map values.
      
      (defn make-store [pool conn ns]
        {:pool pool
         :conn conn
         :ns ns
         :empty empty
         :add-instance add-instance
         :get-interval get-interval
         :get-last get-last})
      

      ;;; in a seperate test file, you can now test the 'OO' implementation
      
      (ns redis-ex.test-history1
         (:require [taoensso.carmine :as car]
                   [redis-ex.history :as hist]))
      (def store
         (hist/make-store
         (car/make-conn-pool)
         (car/make-conn-spec)
         "test"))
      
        (require '[hara.fn :as f])
        (f/send store :empty ["test"])
        ;; => 1
      
        (f/send store :get-instance ["test"] 100000) 
        ;; => nil
      
        (f/send store :add-instance ["test"]
         {100000 {:timestamp 1000000 :data 23.4}
          200000 {:timestamp 2000000 :data 33.4}
          300000 {:timestamp 3000000 :data 43.4}
          400000 {:timestamp 4000000 :data 53.4}
          500000 {:timestamp 5000000 :data 63.4}})
        ;; => [1 1 1 1 1]
      

      构建抽象

      所以因为make-store 函数构造了一个完全自包含的store 对象,所以可以定义函数来利用这一点

      (ns redis-ex.app
         (:require [hara.fn :as f]))
      
      (defn get-last-3-elements [st kdir]
         (f/send st :get-last kdir 3))
      

      如果你想使用它......你会做这样的事情:

      (ns redis-ex.test-app0
        (:use redis-ex.app 
              redis-ex.history)
        (:require [taoensso.carmine :as car]))
      
      (def store
         (hist/make-store
         (car/make-conn-pool)
         (car/make-conn-spec)
         "test"))
      
      (get-last-3-elements ["test"] store) 
      ;;=> [{:timestamp 3000000 :data 43.4} {:timestamp 4000000 :data 53.4} {:timestamp 5000000 :data 63.4}]
      

      用 clojure 模拟 - 'OO' 风格

      所以这样做的真正优势在于get-last-3-elements 方法可以位于完全不同的命名空间中。它根本不依赖于数据库实现,因此现在测试这个功能只需要一个轻量级的工具。

      模拟然后定义是微不足道的。无需加载任何数据库即可完成 redis-ex.usecase 命名空间的测试。

      (ns redis-ex.test-app1
        (:use redis-ex.app))
      
      (defn make-mock-store []
         {:database [{:timestamp 5000000 :data 63.4} 
                     {:timestamp 4000000 :data 53.4}
                     {:timestamp 3000000 :data 43.4} 
                     {:timestamp 2000000 :data 33.4} 
                     {:timestamp 1000000 :data 23.4}]
          :get-last (fn [store kdir number] 
                        (->> (:database store)
                             (take number)
                             reverse))})
      
      (def mock-store (make-mock-store))
      (get-last-3-elements ["test"] mock-store)
      ;; => [{:timestamp 3000000 :data 43.4} {:timestamp 4000000 :data 53.4} {:timestamp 5000000 :data 63.4}]
      

      【讨论】:

      • 确实如此。这就是done in Scheme。 “对象构造函数”是一个返回方法调度函数的函数,它代表对象。 “向对象发送消息”是使用适当的方法名称和参数调用该调度函数。如果对象的内部状态被方法调用修改,您可能会收到一个新的调度函数来替换“旧对象”....
      • ... dispatch 函数持有的方法是闭包,可以访问在 dispatch 函数创建时有效的上下文,对应于 Java 中的对象字段,只有无法访问,因为没有办法直接访问该上下文。在 Clojure 中,dispatch 函数可以用 map 代替(可以像函数一样使用,但看起来更好,并且可以很容易地打补丁)。 At little example 这里基于来自 Clojure 的 Joy 的代码。
      • 是的。确切地。这个例子很棒。
      【解决方案6】:

      较早的帖子将这个问题作为一个关于在 Clojure 中实现对面向对象编程的各种特性的特定支持的价值和可能性的问题。但是,有一系列与该术语相关的属性。并非所有面向对象的语言都支持所有这些。而且 Clojure 直接支持其中一些属性,无论您是否想称其支持“面向对象”。我会提到其中的几个属性。

      Clojure 可以使用其多方法系统支持dispatch on hierarchically defined types。基本功能为defmultidefmethod。 (也许这些在第一次回答问题时不可用。)

      CLOS 相对不寻常的特性之一是它支持对多个参数类型进行分派的函数。 Clojure 非常自然地模拟了这种行为,例如 here 所建议的。 (该示例本身并未使用类型——但这是 Clojure 多方法灵活性的一部分。与第一个示例 here 进行比较。)

      【讨论】:

        【解决方案7】:

        CljOS 是 Clojure 的玩具 OOP 库。完全没有这个词的意义。只是为了好玩。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-09-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多