Clojure中的CLOS是什么?

25
7个回答

19

你考虑过Clojure的数据类型(特别是defrecord)、协议多方法吗?这三个在Clojure中总是比在这些机制之上移植CLOS更符合惯用语。


31
再说了,如果我们都听从有人说“别做这个,这很愚蠢”的话,那么 Steve Russell 或许会听从 John McCarthy 的建议,不再手动编译 eval,而我们也可能在计算机上永远没有 Lisp。因此,开始了一段长期的 Lisp 黑客历史,他们不尊重别人所说的正确做法!所以我说,去吧,将 CLOS 移植到 Clojure 上,看看会发生什么。真的,最坏的情况是什么呢? :-) - Ken
8
我并不是在说,“不要重新实现CLOS”。我只是想说,在尝试像对待奶牛一样对待马之前,应该至少考虑一下语言核心提供和支持的设施是否适合特定用途。现在,如果有人勇敢(或者说愚蠢?),可以在这些核心设施上构建各种对象系统:如果有人如此激动,当然可以构建CLOS,而且很可能还有更先进的路径可供选择。 - cemerick
@cemerick 默认数据类型缺乏继承。isa+multimethods 是最接近的,但你需要自己继承字段。 - desudesudesu
CLOS的调度机制听起来非常复杂,有点过度设计。Clojure协议没有走那么远,而多方法似乎无法访问所有所需的信息。 - David Tonhofer

17

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

  1. Clojure专门设计成在面向对象平台上运行,因此它会吸收底层平台的对象系统。例如,ClojureJVM具有JVM对象系统,ClojureCLR具有CLI对象系统,ClojureScript具有ECMAScript对象系统等。
  2. Rich Hickey讨厌对象。

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

Mikel Evins正在开发一种名为Categories的新型OO方法。他已经为多种Lisp编写了实现,包括Clojure(尽管并不保证所有移植版本都始终是最新的)。

Categories正在逐渐被Bard替代,这是Mikel正在设计的一种新Lisp方言,其内置了Categories。(然后,Bard可能成为Closos的实现语言,这是Mikel提出的一种设计操作系统的想法。)


5
“Rich Hickey讨厌对象。”我的理解是,Lisp家族的一大“优点”是程序员可以将语言扩展到她想要的任何东西,即使原始作者没有(或不赞成 :-))。我对Rich的杰出语言设计表示敬意,但我相信他会同意,允许用户添加完整的对象系统是Clojure(或任何Lisp)的优势之一。 - Ralph

12

Clojure没有CLOS也不想要它,但你可以实现它。

Clojure希望是不可变的,所以拥有可变的面向对象可能有点愚蠢,但你可以拥有一种面向对象的方式。

有了这三个东西,你应该能够满足所有需要,但大多数情况下,最好只使用普通函数和标准数据结构。


1
我认为每种编程语言都应该有一个初学者模式和一个专家模式。在初学者模式下,可以排除“危险”的特性,如面向对象编程,但专家应该被允许在有限的情况下使用这些特性(由专家确定)。例如,Java不支持C风格的宏,因为James Gosling等人认为它们是危险的。虽然有解决方法,但专家有时会因其缺失而咒骂。 - Ralph

4
使用面向对象编程范式编写代码,可以实现松耦合、模拟和测试。Clojure使这变得非常容易。
我曾经遇到的一个问题是代码依赖于其他代码。如果没有很好地使用Clojure命名空间,它实际上会加剧这个问题。理想情况下,命名空间可以被模拟,但我发现...用命名空间进行模拟存在许多问题。

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

一旦您开始构建越来越大的应用程序,命名空间之间开始相互依赖,如果没有大量的依赖关系,单独测试高级组件会变得非常棘手。大多数解决方案涉及函数重新绑定和其他黑魔法,但问题是,在测试时,原始依赖仍在加载->如果您有一个庞大的应用程序,则会成为一个大问题。


我使用数据库库后,受到启发寻找替代方案。数据库库给我带来了很多痛苦——它们需要很长时间才能加载,并且通常是应用程序的核心。没有将整个数据库、库和相关外设引入测试代码,很难测试应用程序。
您希望能够打包文件,以便依赖于数据库代码的系统部分可以被“替换”。面向对象的设计方法提供了答案。
对不起,答案很长......我想为为什么使用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的演讲后,我意识到“面向对象”并不是邪恶的,而实际上非常有用。

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

基本思想是,如果您想构建一个大型应用程序,必须将“功能”(程序的实质部分)与“连线”(接口和依赖项)分开。依赖关系越少越好。
我使用Clojure哈希映射作为“对象”,因为它们没有库依赖项,并且完全通用(请参见Brian Marick在Ruby中使用相同范例的讲解-http://vimeo.com/34522837)。
要使您的Clojure代码具有“面向对象”的特性,您需要以下函数-(从Smalltalk中窃取的)send,它只会在映射中关联了现有键的函数时分派该函数。
(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))

我提供了一个通用的实用程序库(https://github.com/zcaudate/hara,位于hara.fn名称空间中),其中包含实现。如果您想要自己实现它,则只需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进行模拟 - '面向对象'风格

因此,这样做的真正优点是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}]

非常正确。这就是在Scheme中的做法。 "对象构造函数"是一个返回方法调度函数的函数,该函数代表对象。 "向对象发送消息"是使用适当的方法名称和参数调用该调度函数。如果对象的内部状态由方法调用修改,则可以接收新的调度函数以替换"旧对象"。 - David Tonhofer
调度函数持有的方法是闭包,它们可以访问在创建调度函数时有效的上下文,这对应于Java中的对象字段,但由于没有直接访问该上下文的方式,因此无法访问。在Clojure中,调度函数可以被一个映射替换(它可以像函数一样使用,但看起来更好,并且可以轻松地进行修补)。这里有一个小例子,基于Joy of Clojure中的代码。链接:https://github.com/dtonhofer/joy_of_clojure_notes/blob/master/Chapter%207%20-%20Functional%20programming/standard_bot.clj - David Tonhofer
没错,确实。那个例子很棒。 - zcaudate

3
早期的帖子讨论了一个关于在Clojure中实现各种面向对象编程特性的价值和可能性的问题。然而,有一类属性与该术语相关联。并非所有面向对象语言都支持所有这些属性。无论您是否想将此支持称为“面向对象”,Clojure直接支持其中的一些属性。我将提到其中的几个属性。
Clojure可以使用其多方法系统支持基于层次结构定义类型的分派。基本函数是defmultidefmethod。(也许当问题首次被回答时这些函数还不可用。)
CLOS相对不寻常的一个特性是支持根据多个参数类型进行分发的函数。Clojure非常自然地模拟了这种行为,例如这里所示。 (该示例并不直接使用类型 - 但这是Clojure多方法灵活性的一部分。与第一个示例此处进行比较。)

0

CljOS 是一个用于 Clojure 的玩具 OOP 库。它绝不完整。只是我为了好玩而制作的东西。


-3

这是一篇旧帖,但我想回复一下。

Clojure没有面向对象(OO)支持,也没有CLOS支持。环境的底层对象系统仅在互操作性方面提供了基本支持,而不能用于在Clojure中创建自己的类/对象层次结构。Clojure旨在轻松访问CLR或JVM库,但OOP支持到此为止。

Clojure是一种Lisp语言,支持闭包和宏。有了这两个特性,您可以在几行代码中开发一个基本的对象系统。

现在的问题是,在Lisp方言中,您真的需要OOP吗?我会说不需要和需要。不需要,因为大多数问题都可以在任何Lisp中更优雅地解决,而无需对象系统。我会说需要,因为您仍然需要时不时地使用OOP,那么最好提供一个标准的参考实现,而不是让每个极客都去实现它。

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

这是一本非常好的书,真正把lisp的精髓掌握了。你需要稍微调整一下语法到clojure,但概念保持不变。对于你的问题很重要的是,最后一章展示了如何在lisp中定义自己的对象系统。

顺便提一下,clojure支持不可变性。你可以在clojure中创建一个可变的对象系统,但如果你坚持使用不可变性,即使使用OOP,你的设计也会有所不同。大多数标准的设计模式和构造都是以可变性为前提的。


“Clojure没有面向对象的支持。”……“环境中的基础对象系统只能在互操作性方面提供有限支持,不能用于在Clojure中创建自己的类/对象层次结构。”这绝对不是真的。请参见数据类型(特别是defrecord)、协议和多方法、reify、AOT等。您绝对可以在Clojure中实现接口,例如Iterator。即使在REPL中也可以。 - The Alchemist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接