Clojure позволяет инкапсуляцию и наследование, но можно ли их объединить? - PullRequest
8 голосов
/ 05 октября 2009

Вот слишком упрощенный пример для иллюстрации:

Я могу инкапсулировать детали реализации, такие как использование атома для счетчика:

(defn make-counter
  ([] (make-counter 0))
  ([init-val]
   (let [c (atom init-val)]
     {:get (fn [] @c)
      :++ (fn [] (swap! c inc))})))

Но это означает, что мне нужно переопределить все, чтобы добавить функцию (без наследования):

(defn make-bi-counter
  ([] (make-bi-counter 0))
  ([init-val]
   (let [c (atom init-val)]
     {:get (fn [] @c)
      :++ (fn [] (swap! c inc))
      :-- (fn [] (swap! c dec))})))

Принимая во внимание, что если бы можно было просто расширить одну функцию:

(assoc c :-- (env (:++ c) (fn [] (swap! c dec))))

(def c (make-counter))
(def b (make-bi-counter))
user=> ((:-- b))
-1
user=> ((:-- b))
-2
user=> ((:get b))
-2

Или я мог бы просто выставить атом и иметь независимые функции:

(defn -- [a] (swap! a dec))
(def a (atom 0))
(-- a)

Похоже, что лучше отказаться от инкапсуляции, если желательно «наследование» (или, точнее, расширение).

Ответы [ 2 ]

16 голосов
/ 05 октября 2009

Да, я думаю, что идиоматическое Clojure заключается в том, чтобы отделить ваши данные от ваших функций именно по той причине, что позже вы сможете написать новые функции для работы со старыми данными.

Объединение функций с данными также означает, что вы не сможете изменить свои функции позже, не изменив или заново сгенерировав все свои структуры данных, поскольку эти анонимные функции будут храниться повсеместно. Разрабатывая интерактивно в REPL, я не хотел бы выискивать все мои структуры данных, чтобы исправить их каждый раз, когда я меняю функцию. Замыкания в хэш-картах умны, но довольно хрупки, и я бы не пошел по этому пути, если бы не было действительно веской причины.

Требуется лишь небольшая дисциплина, чтобы определить ваш интерфейс (как функции), а затем не забывать придерживаться вашего интерфейса и не связываться с атомом напрямую. Неясно, какую выгоду вы получите от принудительного сокрытия вещей от себя.

Если вы хотите наследовать, мультиметоды - хороший способ сделать это.

(defmulti getc type)
(defmulti ++ type)
(defmulti -- type)

(derive ::bi-counter ::counter)

(defn make-counter
  ([] (make-counter 0))
  ([init-val]
     (atom init-val :meta {:type ::counter})))

(defn make-bi-counter
  ([] (make-bi-counter 0))
  ([init-val]
     (atom init-val :meta {:type ::bi-counter})))

(defmethod getc ::counter [counter]
  @counter)

(defmethod ++ ::counter [counter]
  (swap! counter inc))

(defmethod -- ::bi-counter[counter]
  (swap! counter dec))

1010 *, например *

user> (def c (make-counter))
#'user/c
user> (getc c)
0
user> (def b (make-bi-counter))
#'user/b
user> (++ c)
1
user> (++ b)
1
user> (-- b)
0
user> (-- c)
; Evaluation aborted.
;;  No method in multimethod '--' for dispatch value: :user/counter
0 голосов
/ 21 ноября 2010

Я уверен, что это не идиоматический Clojure, но вы точно можете смоделировать защищенные переменные.

В вашем примере c - это имитированная приватная переменная. Если вы хотите, чтобы это была защищенная переменная, вам нужно определить ее так, чтобы к ней могли обращаться и make-counter , и make-bi-counter . Передайте хеш с именем secret в make-counter . Если secret содержит c , используйте это. Создайте свой собственный в противном случае.

Затем make-bi-counter может создать секретный объект, который содержит c и передать его в конструктор make-counter, Теперь и make-bi-counter и make-counter имеют доступ к одному и тому же c .

...