Как вы можете расширить протокол 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 лучшим решением для вашей проблемы.