clojure / clojure-site

clojure.org site
https://clojure.org
Eclipse Public License 1.0
249 stars 271 forks source link

Protocols implementation affordances using defrecord/deftype vs. extend-protocol #216

Open raymcdermott opened 7 years ago

raymcdermott commented 7 years ago

When implementing Protocols using defrecord there are some missing affordances.

As an example, :pre and :post conditions cannot be applied.

[ Yes, I know we will soon have spec but these affordances will not be deprecated AFAIK. ]

Affordances not available on defrecord or deftype

(defprotocol Squarer (square [x]))

(defrecord PosIntSquarer [x]
  Squarer
  (square [_] (* x x)))

; Usage
(defprotocol Squarer (square [x]))
;=> Squarer

(defrecord PosIntSquarer [x]
  Squarer
  (square [_] (* x x)))
;=> practice1.core.PosIntSquarer

(square (->PosIntSquarer 2))
;=> 4
;---^^^ All good

(square (->PosIntSquarer "A"))
;CompilerException java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number, compiling:(.../core.clj:162:1) 
;---^^^ We would like to prevent this using a simple assertion

(defrecord PosIntSquarer [x]
  Squarer
  (square [_]
    {:pre  [(pos-int? x)]}
    (* x x)))
;=> practice1.core.PosIntSquarer

(square (->PosIntSquarer "A"))
;CompilerException java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number, compiling:(.../core.clj:162:1) 
;---^^^ The pre-condition is being ignored 

(defrecord PosIntSquarer [x]
  Squarer
  (square [_]
    {:pre  [(pos-int? x)]
     :post [(pos? %)]}
    (* x x)))
CompilerException java.lang.RuntimeException: Unable to resolve symbol: % in this context, compiling:(.../core.clj:158:13) 
;---^^^ The post condition compilation fail shows that all affordances are not available

; Calling externally declared functions is one solution

(defn positive-square [x]
  {:pre  [(pos-int? x)]
   :post [(pos? %)]}
  (* x x))
;=> #'practice1.core/positive-square

(defrecord PosIntSquarer [x]
  Squarer
  (square [_]
    (positive-square x)))
;=> practice1.core.PosIntSquarer

(square (->PosIntSquarer "A"))
;CompilerException java.lang.AssertionError: Assert failed: (pos-int? x), compiling:(.../core.clj:163:1) 

Affordances are available via extend-protocol

(defrecord DirectSquarer [x])
=> practice1.core.DirectSquarer
(extend-protocol Squarer
  DirectSquarer
  (square [this]
    {:pre  [(pos-int? (:x this))]
     :post [(pos? %)]}
    (* (:x this) (:x this))))
;=> nil

(square (->PosIntSquarer 2))
;=> 4

(square (->PosIntSquarer "A"))
;CompilerException java.lang.AssertionError: Assert failed: (pos-int? x), compiling:(.../core.clj:178:1) 

(square (->PosIntSquarer -2))
;CompilerException java.lang.AssertionError: Assert failed: (pos-int? x), compiling:(.../core.clj:176:1) 

I don't want to claim this is a bug. But it's a sign that these options have pros and cons which are not currently well explained.

I would like to develop a more comprehensive table of features / affordances might be nicer than the current bullet list

puredanger commented 7 years ago

Thanks for all this Ray - just want to let you know I see this and the other issue and would like to make these things better, but will likely not have time to look at these properly till post-Conj.

raymcdermott commented 7 years ago

Great! There's no hurry :)

In the spirit of reducing your workload, can I do anything to move it forward independently?

PS. I have signed the RHCA

puredanger commented 6 years ago

I would love to have a more comprehensive overview of where things like varargs / multiarity / prepost / etc are available across the different kinds of invocable things (functions, macros, protocols, multimethods, ??). All of this is good content - what would help is having a PR I can review.

I think most of this stuff would be more applicable on a Protocols guide (not the reference page, although a brief statement there might also be ok), but we don't have such a thing right now.