What does retag parameter in s/multi-spec mean?
Asked Answered
D

2

11

Can you explain with examples how does retag parameter impacts multi-spec creation? I find multi-spec documentation hard to digest.

Delora answered 29/7, 2017 at 22:11 Comment(0)
G
6

From the docstring:

retag is used during generation to retag generated values with matching tags. retag can either be a keyword, at which key the dispatch-tag will be assoc'ed, or a fn of generated value and dispatch-tag that should return an appropriately retagged value.

If retag is a keyword (as in the spec guide example), multi-spec internally creates a function here which is used in the generator implementation function. For example, these two multi-spec declarations are functionally equivalent:

(s/def :event/event (s/multi-spec event-type :event/type))
(s/def :event/event (s/multi-spec event-type
                                  (fn [genv tag]
                                    (assoc genv :event/type tag))))

Passing a retag function wouldn't seem like a very useful option given the guide's example, but is more useful when using multi-spec for non-maps. For example, if you wanted to use multi-spec with s/cat e.g. to spec function args:

(defmulti foo first)
(defmethod foo :so/one [_]
  (s/cat :typ #{:so/one} :num number?))
(defmethod foo :so/range [_]
  (s/cat :typ #{:so/range} :lo number? :hi number?))

foo takes either two or three args, depending on the first arg. If we try to multi-spec this naively using the s/cat keyword/tag, it won't work:

(s/def :so/foo (s/multi-spec foo :typ))
(sgen/sample (s/gen :so/foo))
;; ClassCastException clojure.lang.LazySeq cannot be cast to clojure.lang.Associative

This is where being able to pass a retag function is useful:

(s/def :so/foo (s/multi-spec foo (fn [genv _tag] genv)))
(sgen/sample (s/gen :so/foo))
;=>
;((:so/one -0.5)
; (:so/one -0.5)
; (:so/range -1 -2.0)
; (:so/one -1)
; (:so/one 2.0)
; (:so/range 1.875 -4)
; (:so/one -1)
; (:so/one 2.0)
; (:so/range 0 3)
; (:so/one 0.8125))
Gilt answered 9/10, 2017 at 15:30 Comment(1)
Thank you for great explanation and examples!Delora
W
1

I agree the docs are terse!

I wanted to generate a multi-specd map with a tag which could have several values. I found that the second argument passed to the retag function is actually the dispatch tag, not the assigned tag (just like the docs say, in retrospect). This was causing s/gen to generate maps tagged with the (non-default) multimethod dispatch options only, not with the full ranged covered by the tag spec.

(s/def ::tag #{:a :b :c :d})
(s/def ::example-key keyword?)
(s/def ::different-key keyword?)

(defmulti tagmm :tag)
(defmethod tagmm :a [_]
  (s/keys :req-un [::tag ::example-key]))
(defmethod tagmm :default [_] ; this is `defmulti`'s :default
  (s/keys :req-un [::tag ::different-key]))

(s/def ::example (s/multi-spec tagmm :tag))

(gen/sample (s/gen ::example))
;=> only gives examples with {:tag :a, ...}

Providing a retag which just ignored its second argument and returned the generated value made the generator work as expected.

(s/def ::example (s/multi-spec tagmm (fn [gen-v tag] gen-v)))
;=> now gives examples from every ::tag

Hard to work out, but worth it!

Winebaum answered 13/8, 2018 at 12:42 Comment(1)
I think if you have (s/def ::tag keyword?) then you can have (s/def ::example (s/multi-spec tagmm ::tag)) (Note the qualified keyword!)Payee

© 2022 - 2024 — McMap. All rights reserved.