malli icon indicating copy to clipboard operation
malli copied to clipboard

m/walk doesn't preserve symbolic references if metadata is present

Open tekacs opened this issue 4 years ago • 2 comments

When walking types, m/walk is pretty destructive, I've found:

user> (m/walk [:schema {:registry {:a/x 'int?}} :a/x] (m/schema-walker identity))
=> [:schema {:registry #:a{:x int?}} :a/x]

user> (m/walk [:schema {:registry {:a/x 'int?}} [:a/x {}]] (m/schema-walker identity))
=> [:schema {:registry #:a{:x int?}} int?]

user> (m/walk [:schema {:registry {:a/x 'int?}} [:a/x {}]] (fn [a b c d] a))
=> [:schema {:registry #:a{:x int?}} int?]

Note that in the latter cases, the symbol reference to the type :a/x has been dereferenced in the output.

This is with ::m/walk-refs, ::m/walk-schema-refs and ::m/walk-entry-vals all unset (I believe -- my options map is nil).

Do we think it might be practical to have the walker preserve structure, such that

(m/walk ?schema (m/schema-walker identity))

would return a value that's (mu/equals ...) the original?

tekacs avatar Jan 17 '21 00:01 tekacs

To provide an example of something that's kinda hard to do because of this state of affairs:

I'd like to write a function which walks a schema like this:

(1) [:map [:k/d [:map [:a/x {} :a/x]]]]

and rewrites uses of the type :a/x to another type (say :b/x) without changing anything else, so:

(2) [:map [:k/d [:map [:a/x {} :b/x]]]]

This works fine on the type:

(3) [:map [:k/d [:map [:a/x :a/x]]]]

but trying to do this on (1) with m/walk simply ends up deref-ing :a/x inline and giving me a result like:

(4) [:map [:k/d [:map [:a/x {} 'int?]]]]

... and as you can see above, even applying identity causes the dereference.

Also even applying println to all the arguments of Walker shows that there's no information I could use to reconstruct :a/x either (this is a simplified example):

(m/-walk
  (m/schema [:a/x {}] {:registry (mr/composite-registry {:a/x 'int?} (m/default-schemas))})
  (reify m/Walker
    (-accept [this schema path options]
      (println "accept" this schema path options) schema)
    (-inner [this schema path options]
      (println "inner" this schema path options) (m/-walk schema this path options))
    (-outer [this schema path children options]
      (println "outer" schema path children options) schema))
  []
  nil)

... yields ...

accept #object[common.malli$eval81436$reify__81437 0x40b235f common.malli$eval81436$reify__81437@40b235f] int? [] nil
outer int? [] [] nil

tekacs avatar Jan 17 '21 04:01 tekacs

I'm realizing that this may have more to do with how schemas are initially constructed?

user> (def options {:registry (mr/composite-registry (m/default-schemas) {:a/x 'int?})})

user> (m/schema :a/x options)
:a/x

user> (m/schema [:a/x {}] options)
int? ; (this is the m/form of this schema too)

;; because:
user> (@#'m/-schema :a/x options)
int?

;; due to the `vector?` branch of `m/schema`, which performs `m/-schema`, which performs `m/-lookup`

Even more so than above, this seems like a case where the form should reflect the way in which this was constructed?


In this case:

[:schema {:metadata :goes-here} :a/x]

the :a/x is preserved, whereas:

[:a/x {:metadata :goes-here}]

it's not.

tekacs avatar Jan 18 '21 06:01 tekacs