red onions, will watch in full later today 🙂
I would be great if the schemas would always know their local registry. If we fully deref the smallest example, you get a Schema form that can’t be serialized due to hidden registry:
(m/deref-all
[:schema {:registry {::a [:seqable [:ref ::a]]}}
::a])
; => [:seqable [:ref :user/a]]is the :schema needed in your example?
; => [:seqable {:registry {:user/a [:seqable [:ref :user/a]]}} :user/a]oh, missed this: https://github.com/metosin/malli/issues/1088, good explanation of this 👍
I've never thought about how to achieve this with dynamic scope, it might be difficult and/or ugly to get completely right. I don't have a good feeling about it.
Say we had another property :let for lexical scoping that worked in the way we're talking about:
(m/deref [:schema {:let {::a [:seqable ::a]}}
::a])
=> [:seqable [:schema {:let {::a [:seqable ::a]}}
::a]]
I know how to implement that. You don't need :ref because you eliminate the recursive positions whenever you "unfold" the type (they are substituted to the original type).I don't think we should change how local registries work, I'm curious what the mood might be for deprecating :registry though? In preference to something like :let above.
I can see a world where they coexist if we're careful enough though. But the shortcomings of :registry might not be fully surmountable and might always come with these caveats.
Read what is in the gh repo docs so far. Looking forward to reading more of the walkthrough when complete!
thanks Mike! it raised a lot of questions, not sure what I'm going to tackle next.
I think nailing down the representation of lexical scope is a good start.
I finally found a real-world example that cannot be solved by dynamic scope https://github.com/metosin/malli/issues/1088
So I have renewed interest after wondering if this was all theoretical. e.g., This otherwise-useful transformation would cause utter chaos with dynamically scoped registries. https://github.com/metosin/malli/pull/1086
this is the smallest example of the root cause of the problem.
(m/deref [:schema {:registry {::a [:seqable [:ref ::a]]}}
::a])
;;actual
=> ::a
;;preferred
=> [:seqable [:schema {:registry {::a [:seqable [:ref ::a]]}}
::a]]Is there a good pattern for making this schema less verbose? specifically relating to the shared elements of the multi-schema:
(def test-schema [:multi {:dispatch :type}
[:ns/type-a [:map [:id 'string?] [:type ['qualified-keyword? {:namespace :ns}]] [:details 'nil?]]]
[:ns/type-b [:map [:id 'string?] [:type ['qualified-keyword? {:namespace :ns}]] [:details 'int?]]]
[:ns/type-c [:map [:id 'string?] [:type ['qualified-keyword? {:namespace :ns}]] [:details 'string?]]]
[:ns/type-d [:map [:id 'string?] [:type ['qualified-keyword? {:namespace :ns}]] [:details 'any?]]]
])```
the only part that differs is the schema of the :details key, the rest is identical for every type
if you have ideas on how to make a good data syntax to simplify this, I’m all ears. Have a old draft of optionally supporting :extends property for maps. Not sure if that is a good idea…
I’m not sure I have any concept of how feasible/reasonable something like this would be, but it would be great if I could just use the multi-schema dispatch on the [:details…] .. there’s probably complexity in having the dispatch value be a sibling property and not a child of the [:details…], but the pattern shows up often enough in the work I do that i’d love a cleaner way to achieve it.
a direction to look into might be to enhance :merge with the ability to handle :multi . Maybe when you compile a validator for such a schema, the dispatch/s of the child multi's are pushed up to the top level.
[:merge
[:map [:a :int]]
[:multi {:dispatch :bar}
[:baz [:map [:bar :baz]]
[:flub [:map [:bar :flub]]
this would compile a validator similar to
[:multi {:dispatch :bar}
[:baz [:map [:a :int] [:bar :baz]]
[:flub [:map [:a :int] [:bar :flub]]
you'd push the merge into the multi as the intermediate step:
[:multi {:dispatch :bar}
[:baz [:merge [:map [:a :int]] [:map [:bar :baz]]]
[:flub [:merge [:map [:a :int]] [:map [:bar :flub]]]
this might event work for parsing, I don't think
:merge or :multi are path elements in the parsed data structure output (therefore it's fine to manipulate their positions).:multi clauses are surfaced in the result of m/parse so we'll need to figure out a stable way to do that. e.g., :multi 's are nested left-to-right, [:merge :dispatch1 :dispatch2 :dispatch3] => [:dispatch1 [:dispatch2 [:dispatch3 :merge]]]
I'm guessing we'd build a protocol for a "mergable" type that multi and others would extend (maybe :or?).
feels a bit more general than mergable, maybe distributive.
@rdonahue could you try my branch and tell me if it's what you're after?
Love the approach, commented the issue.
@ambrosebs I will try that now, might take me a minute to figure out how to build it 🙂 relatively new to things off the beaten path with dependencies!
@rdonahue yeah wow it's not easy, let me push an installer script to my branch.
No worries, I got a test project up and running.. I was going to need to figure out how to add local deps to a cljs project at some point, and between theller’s smart error messages and @p-himik’s help, I have gotten as far as validating :int 🙂
nice. well I added a ./bin/install script to my branch to install a local snapshot.
I don't use Malli, but that schema looks like Just Data™ to me. You can probably use a for with a case for the different bit - if you really want to avoid ~15 lines of repeated code, that is
@ambrosebs back at it, sent you an invite to my testing repo, still working on it, but I think it’ll do exactly what I need it to 🙂