22 votos

¿CLOS para Clojure?

¿Existe algo como CLOS (Common Lisp Object System) para Clojure?

18voto

cemerick Puntos 3279

¿Ha pensado en el sistema Clojure tipos de datos (especialmente defrecord ), protocolos y multimétodos ? Los tres serán siempre más idiomáticos dentro de Clojure que un puerto de CLOS sobre estos mecanismos.

17voto

Jörg W Mittag Puntos 153275

Clojure no tiene un sistema de objetos, por dos razones:

  1. Clojure está diseñado específicamente para ser alojado en una plataforma orientada a objetos y entonces simplemente absorbe el sistema de objetos de la plataforma subyacente. Por ejemplo, ClojureJVM tiene el sistema de objetos de la JVM, ClojureCLR tiene el sistema de objetos de la CLI, ClojureScript tiene el sistema de objetos de ECMAScript, etc.
  2. Rich Hickey odia los objetos.

Pero, tú puede obviamente implementar un sistema de objetos en Clojure. Clojure es, después de todo, Turing-completo.

Mikel Evins está trabajando en un nuevo enfoque de OO al que llama Categorías . Tiene implementaciones para varios Lisps, incluyendo Clojure (aunque no se garantiza que todos los puertos estén actualizados todo el tiempo).

Las categorías están siendo lentamente subsumidas por Bard un nuevo dialecto de Lisp que Mikel está diseñando y que tiene incorporadas las categorías. (Que, a su vez, puede convertirse en el lenguaje de implementación de Closos una idea de Mikel sobre cómo diseñar un sistema operativo).

11voto

nickik Puntos 3112

Clojure no tiene CLOS y no quiere CLOS pero podrías implementarlo.

Clojure quiere ser inmutable por lo que tener OO mutable sería algo estúpido, pero se puede tener un tipo de OO.

Con estas tres cosas deberías ser capaz de satisfacer todas tus necesidades, pero la mayoría de las veces, lo mejor es utilizar las funciones normales y las estructuras de datos estándar.

4voto

zcaudate Puntos 3659

El uso del paradigma OO es ideal para escribir código poco acoplado, mocking y testing. Clojure hace que esto sea muy fácil de lograr.

Un problema con el que me había topado en el pasado era que el código dependía de otro código. Los namespaces de Clojure exacerban el problema si no se usan bien. Idealmente, los espacios de nombres pueden ser burlados, pero como he encontrado... hay un montón de problemas con la burla de los espacios de nombres:

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

Una vez que empiezas a construir aplicaciones más y más grandes, los espacios de nombres empiezan a depender unos de otros y se vuelve realmente difícil probar tus componentes de nivel superior por separado sin tener un montón de dependencias. La mayoría de las soluciones implican la revinculación de funciones y otra magia negra, pero el problema es que cuando llega el momento de las pruebas, las dependencias originales se siguen cargando -> lo que se convierte en un gran problema si tienes una aplicación grande.


Me motivó a buscar alternativas después de utilizar las bibliotecas de bases de datos. Las librerías de bases de datos me han traído mucho dolor - tardan mucho en cargar y normalmente están en el núcleo de tu aplicación. Es muy difícil probar tu aplicación sin llevar toda la base de datos, la biblioteca y los periféricos asociados a tu código de prueba.

Usted quiere ser capaz de empaquetar sus archivos para que las partes de su sistema que dependen de su código de base de datos puedan ser "intercambiadas". La metodología de diseño OO ofrece la respuesta.

Siento que la respuesta sea bastante larga... Quería dar una buena justificación para por qué El diseño OO se utiliza más que cómo se utiliza. Así que había que utilizar un ejemplo real. He intentado mantener el ns para que la estructura de la aplicación de ejemplo se mantenga lo más clara posible.

código de estilo clojure existente

Este ejemplo utiliza carmine que es un cliente de redis. Es relativamente fácil de trabajar y es rápido de poner en marcha en comparación con korma y datomic, pero una biblioteca de base de datos sigue siendo una biblioteca de base de datos:

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

código de prueba existente

todas las funciones deben ser probadas... esto no es nada nuevo y es código clojure estándar

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

mecanismo de envío orientado a objetos

La idea de que la "OO" no es mala, sino que en realidad es bastante útil, se me ocurrió después de ver esta charla de Misko Hevery:

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

La idea básica es que si quieres construir una gran aplicación, tienes que separar la "funcionalidad" (las tripas del programa) del "cableado" (las interfaces y las dependencias). Cuantas menos dependencias, mejor.

Utilizo los mapas hash de clojure como "objetos" porque no tienen dependencias de la biblioteca y son completamente genéricos (véase Brian Marick hablando sobre el uso del mismo paradigma en Ruby - http://vimeo.com/34522837 ).

Para que tu código clojure esté 'orientado a objetos' necesitas la siguiente función - ( send robado de smalltalk) que sólo despacha una función asociada a una clave en un mapa si está asociada a una clave existente.

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

Proporciono la implementación en una biblioteca de utilidad de propósito general (https://github.com/zcaudate/hara en el hara.fn espacio de nombres). Son 4 líneas de código si quieres implementarlo por ti mismo.

definir el objeto "constructor

ahora puede modificar el original make-store para añadir funciones en el mapa. Ahora tienes un nivel de indirección.

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

construir la abstracción

así que porque el make-store construye un store que es completamente autónomo, se pueden definir funciones para aprovechar este

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

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

y si quieres usarlo... harías algo como:

(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 con clojure - estilo 'OO'

Así que la verdadera ventaja de esto es que el get-last-3-elements puede estar en un espacio de nombres completamente diferente. no depende de la implementación de la base de datos en absoluto, por lo que probar esta función ahora sólo requiere un arnés ligero.

Los mocks son entonces triviales de definir. Las pruebas del espacio de nombres redis-ex.usecase pueden realizarse sin cargar ninguna biblioteca de base de datos.

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

3voto

Mars Puntos 1622

Los posts anteriores abordan la cuestión como una pregunta sobre el valor y las posibilidades de implementar un soporte específico para varias características de programación orientada a objetos en Clojure. Sin embargo, hay una familia de propiedades que se asocian a ese término. No todos los lenguajes orientados a objetos las soportan todas. Y Clojure soporta directamente algunas de estas propiedades, tanto si quieres llamar a ese soporte "orientado a objetos" como si no. Mencionaré un par de estas propiedades.

Clojure puede soportar envío en tipos definidos jerárquicamente utilizando su sistema multimétodo. Las funciones básicas son defmulti y defmethod . (Tal vez no estaban disponibles cuando se respondió a la pregunta por primera vez).

Una de las características relativamente inusuales de CLOS es su soporte para funciones que despachan sobre los tipos de múltiples argumentos. Clojure emula ese comportamiento de forma muy natural, como por ejemplo aquí sugiere. (El ejemplo no utiliza tipos en sí, pero eso es parte de la flexibilidad de los multimétodos de Clojure. Compara con el primer ejemplo aquí .)

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X