clojuremultimethodclojure.spec

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


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


Solution

  • 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))