Clojure: добавление функций для записи без определения нового протокола
Я привык к ОО в Python/ Java. Делаю Clojure сейчас. Я столкнулся с defrecord, но, похоже, мне нужно определить протокол для каждой функции или набора функций, которые я хочу реализовать в записи. Создание нового протокола создает трения. Я должен назвать не только функцию, которую я хочу, но и протокол. То, что я ищу, - это способ "красиво" связать функцию с записью, чтобы функция имела доступ к параметрам записи через этот параметр, без необходимости определять новый протокол или добавлять функцию к существующему протоколу.
3 ответа
Отличный вопрос.
Как обычно, есть прекрасный способ сделать что-то в Clojure - вот как реализовать собственную простую динамическую ОО-систему (включая наследование, полиморфизм и инкапсуляцию) в 10 строках Clojure.
Идея: Вы можете поместить функции в карты или записи Clojure нормалей, если хотите, создавая ОО-подобную структуру. Затем вы можете использовать это в стиле "прототип".
; define a prototype instance to serve as your "class"
; use this to define your methods, plus any default values
(def person-class
{:get-full-name
(fn [this] (str (:first-name this) " " (:last-name this)))})
; define an instance by merging member variables into the class
(def john
(merge person-class
{:first-name "John" :last-name "Smith"}))
; macro for calling a method - don't really need it but makes code cleaner
(defmacro call [this method & xs]
`(let [this# ~this] ((~method this#) this# ~@xs)))
; call the "method"
(call john :get-full-name)
=> "John Smith"
; added bonus - inheritance for free!
(def mary (merge john {:first-name "Mary"}))
(call mary :get-full-name)
=> "Mary Smith"
Если вы еще не пробовали мультиметоды, они могут быть ближе к тому, что вы ищете.
Определение:
(defrecord Person [first middle last])
(defmulti get-name class)
(defmethod get-name Person [person] (:first person))
Использование:
(def captain (Person. "James" "T" "Kirk"))
(get-name captain)
Выбранная реализация мультиметода основана на функции диспетчеризации в defmulti (функция, которая принимает аргументы, переданные функции и возвращает значение диспетчеризации). Обычно "класс" - это функция отправки, как здесь, для отправки по типу. Мультиметоды поддерживают несколько независимых иерархий типа ad-hoc или на основе Java, реализации по умолчанию и т. Д.
В целом, я думаю, что, возможно, вы захотите сделать шаг назад и подумать, действительно ли вам нужны протоколы или мультиметоды. Вы, кажется, пытаетесь "сделать OO" в Clojure. Хотя аспекты ОО (такие как полиморфизм) велики, возможно, вам следует попробовать по-другому думать о своей проблеме. Например, в только что приведенном примере нет убедительной причины (пока) для полиморфной реализации get-name. Почему бы просто не сказать:
(defn get-name [x] (:first x))
Вам вообще нужна запись о человеке? Будет ли достаточно простой карты? Иногда ответы да, иногда нет.
В целом Clojure не обеспечивает наследование классов. Вы, конечно, можете создать эквивалент (даже с протоколами), если вы действительно этого хотите, но в целом я считаю, что есть другие, более эффективные способы решения этой проблемы в Clojure.
Используя идею Микера, я разработал способ получить это (ОО-подобные классы)
;; -----------------------------------------
;; main()
;; -----------------------------------------
(def p1 (newPoint 3 4))
(def p2 (newPoint 0 0))
(call p1 :getY) ;; == 4
(call p1 :distance p2) ;; == 5
Полный пример с "приличным и организованным" способом объявления OO-подобного класса
;; -----------------------------------------
;; begin Point class
;; -----------------------------------------
(defrecord Point [x y methods] )
(def someMethods
{
:getX (fn [this] (:x this) )
:getY (fn [this] (:y this) )
:distance (fn [this other]
(def dx (- (:x this) (:x other)))
(def dy (- (:y this) (:y other)))
(Math/sqrt (+ (* dx dx) (* dy dy) ))
)
}
)
;;
;; Point constructor
;;
(defn newPoint [x y]
(Point. x y someMethods)
)
;; -----------------------------------------
;; end Point class
;; -----------------------------------------
;; -----------------------------------------
;; helper to call methods
;; -----------------------------------------
(defn call
([obj meth] ((meth (:methods obj)) obj))
([obj meth param1] ((meth (:methods obj)) obj param1))
([obj meth param1 param2] ((meth (:methods obj)) obj param1 param2))
)
;; -----------------------------------------
;; main()
;; -----------------------------------------
(def p1 (newPoint 3 4))
(def p2 (newPoint 0 0))
(call p1 :getY) ;; == ((:getX (:methods p1)) p1)
(call p1 :distance p2) ;; == ((:distance (:methods p1)) p1 p2)