Как вы можете расширить протокол Clojure к другому протоколу?
Предположим, у меня есть два протокола:
(defprotocol A
(f [this]))
(defprotocol B
(g [x y]))
И я хочу расширить протокол B ко всем экземплярам, поддерживающим протокол A:
(extend-protocol A
String
(f [this] (.length this)))
(extend-protocol B
user.A
(g [x y] (* (f x) (f y))))
Основная мотивация заключается в том, чтобы избежать необходимости разделить B отдельно на все возможные классы, которые могут быть расширены до или даже неизвестными будущими классами, которые другие люди могут распространять на A (представьте, если A был частью публичного API, например).
Однако это не сработает - вы получите что-то вроде следующего:
(g "abc" "abcd")
=> #<IllegalArgumentException java.lang.IllegalArgumentException:
No implementation of method: :g of protocol: #'user/B found for
class: java.lang.String>
Возможно ли это вообще? Если нет, есть ли разумное обходное решение для достижения той же цели?
Ответы
Ответ 1
Мне кажется, что вы можете реализовать функцию g
в терминах f
. Если это так, у вас есть весь полиморфизм, который вам нужен без протокола B
.
Я имею в виду следующее, учитывая, что f
является полиморфным, тогда
(defn g [x y]
(* (f x) (f y)))
дает функцию g
, которая поддерживает все типы, реализующие протокол A
.
Часто, когда протоколы находятся в самом низу, простые функции, определенные только в терминах функций протокола (или других функций, которые сами используют протокол) делают все пространство имен/библиотеку/программу очень полиморфными, расширяемыми и гибкими.
Библиотека последовательности - отличный пример этого. Упрощенный, существуют две полиморфные функции, first
и rest
. Остальная библиотека последовательности - это обычные функции.
Ответ 2
Протоколы не являются типами и не поддерживают наследование. Протокол по существу представляет собой именованный набор определений функций (и механизм отправки при вызове этих функций).
Если у вас есть несколько типов, все из которых имеют одну и ту же реализацию, вы можете просто вызвать общую функцию. Кроме того, вы можете создать карту методов и extend
каждого типа с этой картой. Например:.
(defprotocol P
(a [p])
(b [p]))
(deftype R [])
(deftype S [])
(deftype T [])
(def common-P-impl
{:a (fn [p] :do-a)
:b (fn [p] :do-b)})
(extend R
P common-P-impl)
(extend S
P common-P-impl)
(extend T
P common-P-impl)
Если вы предоставите более подробную информацию о своем реальном сценарии, мы сможем предложить правильный подход.
Ответ 3
Из того, что я вижу, протоколы действительно могут расширять протоколы.
Я привел здесь пример: https://github.com/marctrem/protocol-extend-protocol-example/blob/master/src/extproto/core.clj
В примере у нас есть протокол Animalia
(который позволяет его членам делать dream
), который расширяется протоколом Erinaceinae
(который позволяет его членам go-fast
).
У нас есть запись Hedgehog
, которая является частью протокола Erinaceinae
. Наш экземпляр записи может быть как dream
, так и go-fast
.
(ns extproto.core
(:gen-class))
(defprotocol Animalia (dream [this]))
(defprotocol Erinaceinae (go-fast [this]))
(extend-protocol Animalia
extproto.core.Erinaceinae
(dream [this] "I dream about things."))
(defrecord Hedgehog [lovely-name]
Erinaceinae
(go-fast [this] (format "%s the Hedgehog has got to go fast." (get this :lovely-name))))
(defn -main
[& args]
(let [my-hedgehog (Hedgehog. "Sanic")]
(println (go-fast my-hedgehog))
(println (dream my-hedgehog))))
;1> Sanic the Hedgehog has got to go fast.
;1> I dream about things.
Ответ 4
в "Clojure применяется" там рецепт расширения протокола по протоколу
(extend-protocol TaxedCost
Object
(taxed-cost [entity store]
(if (satisfies? Cost entity)
(do (extend-protocol TaxedCost
(class entity)
(taxed-cost [entity store]
(* (cost entity store) (+ 1 (tax-rate store)))))
(taxed-cost entity store))
(assert false (str "Unhandled entity: " entity)))))
на самом деле ничего не мешает вам просто расширять протокол другим
(extend-protocol TaxedCost
Cost
(taxed-cost [entity store]
(* (cost entity store) (+ 1 (tax-rate store)))))
в то время как книга говорит, что это невозможно. Я поговорил с Алексом Миллером об этом, и он сказал следующее:
Это действительно не работает во всех случаях. Протокол генерирует интерфейс Java, и вы можете, конечно, расширить протокол к этому интерфейсу. Проблема в том, что не вся реализация протокола реализует этот интерфейс - только записи или типы, которые делают это с помощью встроенного объявления типа (defrecord Foo [a] TheProtocol (foo ...))
. Если вы реализуете протокол с помощью extend-type
или extend-protocol
, то эти экземпляры НЕ будут улавливаться расширением интерфейса протокола. Итак, вы действительно не должны этого делать:)
Ответ 5
Хотя я не совсем понимаю, что вы пытаетесь сделать, мне интересно, будет ли > multimethods лучшим решением для вашей проблемы.