Fork me on GitHub
#malli
<
2021-12-30
>
ikitommi08:12:43

One of the oldest (and annoying) issue is how to describe a map + map-of. Need to take a stab at it, here are the options: 1) ternary closed

[:map {:closed [:string :int]}
 [:x :int]
 [:y :int]] 
2) new extra-keys (or such)
[:map {:extra-keys [:string :int]}
 [:x :int]
 [:y :int]]
3) ::m/default (like in :multi)
[:map
 [:x :int]
 [:y :int]
 [::m/default [:map-of :string :int]]]

ikitommi08:12:36

leaning on 3, because: • it’s coherent way to describe “default in case none of the defined keys matched” • it’s easy to remove or add the key, e.g. (mu/assoc MyMap ::m/default [:map-of :uuid MyMap]):map-of already supports key-decoding so things like :uuid keys just work oob

ikitommi08:12:59

comments welcome, original issue here: https://github.com/metosin/malli/issues/43

Ben Sless09:12:06

4: [:map ^:of [int? int?]]

ikitommi11:12:58

metadata looks good when writing, not that much when reading / serializing.

Ben Sless11:12:32

I think this example is confusing because the map of spec and the entries don't match. A property of of on the map makes the most sense imo

ikitommi11:12:56

:of sounds ok. what do you mean by: > the map of spec and the entries don’t match.

ikitommi11:12:53

how would a schema in the properties be reported in m/explain?

Ben Sless11:12:51

If its :string then it can't have a keyword key. It has to be a union and not a superset. Which is confusing

Ben Sless11:12:00

But I'm not sure map-of and map should be unified

ikitommi11:12:16

I read it “it’s a map with keyword keys :x and :y, the rest of the keys should be :string -> :int. Same with plumatic:

{:x s/Int, :y s/Int, s/Str s/Int}

ikitommi11:12:10

e.g. {:x 1, :y 2, "z" 3, "å" 4} is valid in it.

Ben Sless11:12:42

As a user who has to work and communicate with others via code, I wouldn't want this to be valid

Ben Sless11:12:34

I'd prefer that the specific keys will be a subset of the key schema and values be a subset of the value schema, not that the entire map describe a union

ikitommi11:12:37

I see you point, would not want to use that myself, but that’s what JSON Schema, Plumatic and many others have atm.

ikitommi11:12:05

for the destructuring, I would like to describe the namespaced keys with that

Ben Sless11:12:01

Json schema has no notion of keywords, though

Ben Sless11:12:22

And I don't mind saying Plumatic made a mistake by allowing it

ikitommi11:12:05

5) wrap the extra keys with something like :schema to mark it’s a schema, not a real key.

[:map
 [:x :int]
 [:y :int]
 [[:schema :string] :int]]

Ben Sless11:12:31

Wouldn't it require a special case in parsing?

Ben Sless11:12:23

Leaving surprises to your future self?

ikitommi11:12:59

yes, I don’t think there is a right answer to this, just compromises. Not a fan of those (the reason the issue has been open for so long)

Ben Sless11:12:34

Should map-of and map schemas be unified? One describes nominal tuples, the other a set of tuples

ikitommi11:12:05

how could/should they be unified?

juhoteperi11:12:41

::m/default makes sense, but using :map-of together with that seems funny. In :map schema the items are key-value pairs, :map-of describes full map. Something like [::m/default [:map-entry :string :int]] or just [::m/default :string :int] or [::m/default [:string :int]] (just force the :map default entry to always have two items) could be cleaner.

ikitommi11:12:21

problem with [::m/default :string :int] is that when you ask for m/children of the map, you get funny results.

juhoteperi11:12:26

Maybe the :map-of makes sense as there can be multiple "default" / extra keys

juhoteperi11:12:24

Maybe using ::m/extra name would describe better that this is the schema for those keys that aren't directly defined in :map schema

ikitommi11:12:25

In JSON Schema -land:

{
  "type": "object",
  "properties": {
    "number": { "type": "number" },
    "street_name": { "type": "string" },
    "street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
  },
  "additionalProperties": { "type": "string" }
}

Ben Sless12:12:04

It reads to me like the specified properties and additional properties should have the same key type

Ben Sless12:12:40

But json is limited

ikitommi12:12:40

yes, we do not have that limitation

ikitommi12:12:00

but, the extra keys are just for the case none of the actual keys hit

ikitommi12:12:11

there is also :patternedProperties and other silly things.

ikitommi12:12:45

the root need for this NOW btw is how to describe the namespaced keys in the destructuring syntax, elegantly. e.g. what is the schema for the map here:

(ns demo)

(let [{:keys [a1] ::keys [a2], :kikka/keys [a3]
       :syms [b1] ::syms [b2] :kikka/syms [b3]
       :strs [c1]
       :or {a1 0} :as map}
      {:a1 1, ::a2 2, :kikka/a3 3
       'b1 4 'demo/b2 5, 'kikka/b3 6
       "c1" 5}]
  [a1 a2 a3 b1 b2 b3 c1])
; => [1 2 3 4 5 6 5]

ikitommi12:12:26

thought that would be a good reason to add the “extra keys” here, but not sure if that is needed. could be just :multi with dispatch on key qualification :thinking_face:

Ben Sless12:12:32

Or map of enum to something?

Ben Sless12:12:06

This brings me back to my question about a schema about entries

Ben Sless12:12:25

entry := qualified | simple | or | as
qualified := [ns/kind form]
simple := [kind form]
kind := keys | syms | strs
or := [:or map-of,,,]
as := [:as symbol]

ikitommi16:01:22

unexpected help for the existing stuff:

(defn -map-like [x]
  (or (map? x)
      (and (seqable? x)
           (every? (fn [e] (and (vector? e) (= 2 (count e)))) x))))

(defn -keys-syms-key [k] 
  (-> k name #{"keys" "syms"}))

(def MapLike
  (m/-collection-schema
   {:type :map-like
    :empty {}
    :pred -map-like}))

(m/parse
 [MapLike
  [:or
   [:tuple [:= :keys] [:vector ident?]]
   [:tuple [:= :strs] [:vector ident?]]
   [:tuple [:= :syms] [:vector ident?]]
   [:tuple [:= :or] [:map-of simple-symbol? any?]]
   [:tuple [:= :as] symbol?]
   [:tuple [:and :qualified-keyword [:fn -keys-syms-key]] [:vector ident?]]]]
 '{:keys [b]
   :strs [c]
   :syms [d]
   :demo/keys [e]
   :demo/syms [f]
   :or {b 0, d 0, f 0} :as map})
;{:keys [b]
; :strs [c]
; :syms [d]
; :demo/keys [e]
; :demo/syms [f]
; :or {b 0, d 0, f 0} :as map}
… not the most performant, but good for the destructuring case 🥳

ikitommi16:01:22

unexpected help for the existing stuff:

(defn -map-like [x]
  (or (map? x)
      (and (seqable? x)
           (every? (fn [e] (and (vector? e) (= 2 (count e)))) x))))

(defn -keys-syms-key [k] 
  (-> k name #{"keys" "syms"}))

(def MapLike
  (m/-collection-schema
   {:type :map-like
    :empty {}
    :pred -map-like}))

(m/parse
 [MapLike
  [:or
   [:tuple [:= :keys] [:vector ident?]]
   [:tuple [:= :strs] [:vector ident?]]
   [:tuple [:= :syms] [:vector ident?]]
   [:tuple [:= :or] [:map-of simple-symbol? any?]]
   [:tuple [:= :as] symbol?]
   [:tuple [:and :qualified-keyword [:fn -keys-syms-key]] [:vector ident?]]]]
 '{:keys [b]
   :strs [c]
   :syms [d]
   :demo/keys [e]
   :demo/syms [f]
   :or {b 0, d 0, f 0} :as map})
;{:keys [b]
; :strs [c]
; :syms [d]
; :demo/keys [e]
; :demo/syms [f]
; :or {b 0, d 0, f 0} :as map}
… not the most performant, but good for the destructuring case 🥳