Clojure 的 CLOS?

发布于 2024-09-29 05:16:42 字数 147 浏览 2 评论 0原文

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

Does there exist anything like CLOS (Common Lisp Object System) for Clojure?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

我家小可爱 2024-10-06 05:16:42

您是否考虑过 Clojure 的数据类型(尤其是 defrecord),协议多方法?这三者在 Clojure 中总是比这些机制之上的 CLOS 端口更惯用。

Have you considered Clojure's data types (especially defrecord), protocols, and multimethods? All three will always be more idiomatic within Clojure than a port of CLOS on top of these mechanisms.

二手情话 2024-10-06 05:16:42

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

  1. Clojure 专门设计为托管在面向对象的平台上,然后它只是吸收底层平台的对象系统。即ClojureJVM有JVM对象系统,ClojureCLR有CLI对象系统,ClojureScript有ECMAScript对象系统,等等。
  2. 里奇·希基讨厌物体。

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

Mikel Evins 正在研究一种新的 OO 方法,他称之为 类别。他实现了多种 Lisp,包括 Clojure(尽管不能保证所有端口始终保持最新)。

类别正在慢慢被 Bard 所涵盖,这是 Mikel 正在设计的一种新的 Lisp 方言,它内置了类别。(然后反过来,可能会成为 Mikel 的一个想法 Closos 的实现语言了解如何设计操作系统。)

Clojure itself doesn't have an object system, for two reasons:

  1. Clojure is specifically designed to be hosted on an object-oriented platform and it then simply absorbs the underlying platform's object system. I.e. ClojureJVM has the JVM object system, ClojureCLR has the CLI object system, ClojureScript has the ECMAScript object system, and so on.
  2. Rich Hickey hates objects.

But, you can obviously implement an object system in Clojure. Clojure is, after all, Turing-complete.

Mikel Evins is working on a new approach to OO which he calls Categories. He has implementations for several Lisps, including Clojure (although not all the ports are guaranteed to be up-to-date all the time).

Categories is slowly being subsumed by Bard, a new Lisp dialect that Mikel is designing, which has Categories built in. (Which then, in turn, may become the implementation language for Closos, an idea Mikel had for how to design an operating system.)

画尸师 2024-10-06 05:16:42

Clojure 没有 CLOS 并且不需要 CLOS,但你可以实现它。

Clojure 希望是不可变的,因此拥有可变的 OO 有点愚蠢,但你可以拥有一种 OO。

有了这三件事,您应该能够满足您的所有需求需要,但大多数时候,最好只使用普通函数和标准数据结构。

Clojure does not have CLOS and doesn't want CLOS but you could implement it.

Clojure wants to be immutable so to have mutable OO would be kind of stupid, but you can have a kind of OO.

With these three things you should be able to fulfill all your needs, but most of the time, its best to just use normal functions and the standard data structures.

寻找一个思念的角度 2024-10-06 05:16:42

使用 OO 范式非常适合编写松散耦合的代码、模拟和测试。 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 代码“面向对象”,您需要以下函数 - (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}]

Using the OO paradigm is ideal for writing loosely coupled code, mocking and testing. Clojure makes this so easy to accomplish.

One problem that I had ran into in the past was code depending on other code. Clojure namespaces actually exacebate the problem if not used well. Ideally, namespaces can be mocked out but as I have found... there are a lot of problems with mocking out namespaces:

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

Once you start building bigger and bigger applications, the namespaces start depending upon each other and it gets really unweldy to test your higher-level components seperately without having a bunch of dependencies. Most of the solutions involve function re-binding and other black magic but the problem is that come testing time, the original dependencies are still being loaded -> which grows into big problem if you have a big application.


I was motivated to to look for alternatives after using database libraries. Database libraries have brought me so much pain - they take ages to load and usually are at the core of your application. Its very difficult to test your application without bring a whole database, the library and associated peripherals into your test code.

You want to be able to package your files so that parts of your system that depend on your database code can be 'swapped out'. OO design methodology provides the answer.

I'm sorry the answer is quite long... I wanted to give a good rationale for why OO design is used more so than how it is used. So an actual example had to be used. I have attempted to keep the ns declarations so that the structure of the example application will be kept as clear as possible.

existing clojure style code

This example uses carmine, which is a redis client. It is relatively easy to work with and is quick to start up compared to korma and datomic, but a database library is still a database library:

(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})

existing test code

all the functions should be tested... this is nothing new and is standard clojure code

(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]

object orientated dispatch mechanism

The idea that 'OO' isn't evil but actually quite useful came to me after watching this talk Misko Hevery:

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

The basic idea is that if you want to build a big application, you have to separate the 'functionality' (the guts of the program) from the 'wiring' (the interfaces and the dependencies). The less dependencies the better.

I use clojure hash-maps as 'objects' because they have no library dependencies and are completely generic (see Brian Marick talking about using the same paradigm in ruby - http://vimeo.com/34522837).

To make your clojure code 'object orientated' you need the following function - (send stolen from smalltalk) which just dispatches a function associated with a key in a map if it is associated with an existing key.

(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))

I provide the implementation in a general purpose utility library (https://github.com/zcaudate/hara in the hara.fn namespace). It's 4 lines of code if you want to implement it for yourself.

defining the object 'constructor'

you can now modify the original make-store function to add functions in the map. Now you have a level of indirection.

;;; 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]

build abstraction

so because the make-store function constructs a store object which is completely self contained, functions can be defined to take advantage of this

(ns redis-ex.app
   (:require [hara.fn :as f]))

(defn get-last-3-elements [st kdir]
   (f/send st :get-last kdir 3))

and if you want to use it... you would do something like:

(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}]

mocking with clojure - 'OO' style

So the real advantage of this is that the get-last-3-elements method can be in a completely different namespace. it does not depend on the database implementation at all and so testing this function now only requires a lightweight harness.

mocks are then trivial to define. Testing of the redis-ex.usecase namespace can be done without loading in any database libraries.

(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}]
似狗非友 2024-10-06 05:16:42

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

Clojure 可以使用其多方法系统支持分派分层定义的类型。基本功能是 defmultidefmethod。 (也许第一次回答问题时这些还不可用。)

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

The earlier posts address the question as a question about the value and possibilities of implementing specific support for various features of object-oriented programming in Clojure. However, there is a family of properties that are associated with that term. Not all object-oriented languages support all of them. And Clojure directly supports some of these properties, whether you want to call that support "object-oriented" or not. I'll mention a couple of these properties.

Clojure can support dispatch on hierarchically defined types using its multimethod system. The basic functions are defmulti and defmethod. (Maybe these weren't available when the question was first answered.)

One of the relatively unusual features of CLOS is its support for functions that dispatch on the types of multiple arguments. Clojure emulates that behavior very naturally, as an example here suggests. (The example doesn't use types per se--but that's part of the flexibility of Clojure's multimethods. Compare with the first example here.)

幽蝶幻影 2024-10-06 05:16:42

CljOS 是 Clojure 的玩具 OOP 库。它绝不是完整这个词的意思。只是我为了好玩而做的东西。

CljOS is a toy OOP library for Clojure. It is by no sense of the word complete. Just something I made to have fun.

街角卖回忆 2024-10-06 05:16:42

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

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

Clojure 是一个 Lisp,支持闭包和闭包。宏。考虑到这两个功能,您可以用几行代码开发一个基本的对象系统。

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

我建议你看一下 Paul Graham 写的 On Lisp 一书。您可以在线免费咨询。

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

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

It's an old post but I wanted to respond to it.

No clojure has no OO support, and no CLOS support. The underlying object system of the environment is only barely available in the sence of interoperability, not for making your own class/objects hierarchies in clojure. Clojure is made for easy access to CLR or JVM libraries, but OOP support end here.

Clojure is a lisp and support closures & macros. With thoses 2 features in mind, you can develop a basic object system in a few lines of code.

Now the point is do you really need OOP in a lisp dialect ? I would say no and yes. No because most problem can be solved without an object system and more elegantly in any lisp. I would say yes, because you'll still need OOP from time to time and it is then better to provide a standard reference implementation than having every geek implementing it's own.

I would recommand that you take a look at On Lisp book, from Paul Graham. You can consult it free of charge online.

This is really a good book, that really grasp the essence of lisp. You'll have to adapt the syntax a little to clojure, but concepts remain the same. Important for your question, one of the last chapter show how to define your own object system in lisp.

A side remark, clojure embrace immutability. You can make a mutable object system in clojure, but if you stick to immutability, you design, even using OOP will be quite different. Most standard design pattern and construction are made with mutability in mind.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文