malli

https://github.com/metosin/malli :malli:
ikitommi 2021-05-03T05:25:38.124100Z

@joel380 not atm. The new malli schemas of malli schemas feature allows us to describe the available properties formally as schemas, but it’s just the mechanism atm, no content shipped. Once all schmas are described (both properties and children), it will be THE documentation. For now, you need to read the sources. Documentation PRs always welcome.

ikitommi 2021-05-03T05:29:20.127400Z

e.g.

;; currently
(m/properties-schema :string)
; => nil

;; after
(m/properties-schema :string)
; [:map
;  [:min {:description "length must be at least"} :int]
;  [:max {:description "length must be at most"} :int]]
… different schema applications (e.g. generation) will have their own overlays, which can be merged to get the full set of available keys. Should be easy to generate proper docs out of those.

👍 1
ikitommi 2021-05-03T05:30:20.128500Z

after the children & property schemas are done, one can also generate valid schema-hiccup out of a given registry 🙂

Yehonathan Sharvit 2021-05-03T12:45:31.129800Z

Is there a way to forbid some combination of keys in a map? For instance a map with either :a and :b or :c and :d

borkdude 2021-05-03T12:48:17.130200Z

probably using another predicate?

borkdude 2021-05-03T12:48:38.130500Z

or using two different maps

Yehonathan Sharvit 2021-05-03T12:51:41.130800Z

What do you mean by “using another predicate”?

borkdude 2021-05-03T12:53:52.131200Z

[:and [:map ...] [:fn (fn [...] ...)]]

borkdude 2021-05-03T12:54:21.131600Z

Same as with clojure.spec

borkdude 2021-05-03T12:54:50.131800Z

(def my-schema
  [:and
   [:map
    [:x int?]
    [:y int?]]
   [:fn (fn [{:keys [x y]}] (> x y))]])

Ben Sless 2021-05-03T12:55:57.132600Z

I was just going to ask if there's a way to specify constraints on relations between keys which does not involve defining an ad-hoc function

borkdude 2021-05-03T12:56:22.133100Z

Maybe using two separate closed maps also works. I think it depends on your domain perhaps?

Ben Sless 2021-05-03T12:57:14.133700Z

Likely, although I was just thinking of a case like the one you illustrated, a constraint where the value at one key is greater than another

Ben Sless 2021-05-03T12:59:19.134300Z

This pattern repeats so often, should it be a schema?

Ben Sless 2021-05-03T13:19:21.136Z

Something like:

(def preds {:> >, :>= >=, :< <, :<= <=, := =, :not= not=})

(defn -rel
  []
  (m/-simple-schema
   (fn [_ [pred a b]]
     {:type :rel,
      :pred (m/-safe-pred #((preds pred) (get % a) (get % b))),
      :min 3,
      :max 3})))

(m/validate
 (m/schema
  [:and
   [:map
    [:x :int]
    [:y :int]]
   [:rel :< :x :y]]
  {:registry
   (mr/composite-registry
    m/default-registry
    {:rel (-rel)})})
 #_{:x 1 :y 2}
 {:x 1 :y 1})

👍 1
2
borkdude 2021-05-03T13:23:18.136700Z

reminds me of jet :)

$ echo  '{:a 1 :b 2}' | jet --query '(< :a :b)'
true
$ echo  '{:a 1 :b 2}' | jet --query '(> :a :b)'
false

👍 1
1
ikitommi 2021-05-03T13:28:47.141300Z

I think it’s good idea to experiment new relational schemas in the user space, both -simple-schema & -collection-schma are good ways to make these, like @ben.sless you demoed. The more stuff pushed into these data-languages, the more it looks like a simple sci.

2021-05-10T02:48:33.272200Z

Typescript is way more pragmatic than elm/Haskell. Structural typing almost looks like malli schemas enforced at compile time.

Ben Sless 2021-05-03T13:29:52.142500Z

I also had some thoughts on the subject here https://clojureverse.org/t/declarative-rules-for-relations-between-inputs/7623/5?u=bsless

Ben Sless 2021-05-03T13:30:32.142800Z

A big plus it could have over sci is getting the benefit of the JIT while still keeping the schema serializable

ikitommi 2021-05-03T13:30:45.143Z

only 👍

😄 1
ikitommi 2021-05-03T13:31:16.143300Z

true, sci introduces a lot of overhead. and is big on cljs.

ikitommi 2021-05-03T13:31:45.143900Z

would meander be great at describing the relations, as data?

borkdude 2021-05-03T13:31:54.144Z

much smaller than self-hosted CLJS though

borkdude 2021-05-03T13:32:09.144200Z

but how many people are really using schema serialization?

borkdude 2021-05-03T13:32:15.144400Z

I think most people are not

ikitommi 2021-05-03T13:32:23.144600Z

i think so too.

Ben Sless 2021-05-03T13:32:38.144800Z

Does select-keys translate to mu/get of all the keys then closing the schema?

ikitommi 2021-05-03T13:33:55.145Z

but a lightweight map key dependency utility might be a good fit for many things. just (somone) needs (to invent) more syntax.

Ben Sless 2021-05-03T13:33:56.145200Z

The schema serialization isn't my first (or third) priority here tbh, it's more about having more expressive schemas for the common 90% of cases and not having to define ad-hoc functions and errors for them, not to mention generators.

👍 1
borkdude 2021-05-03T13:34:35.146Z

yeah, I think that's nice. I think clojure spec also doesn't have a good answer to this yet and it's pretty common

borkdude 2021-05-03T13:36:44.146500Z

malleander!

🤯 2
ikitommi 2021-05-03T13:37:43.147200Z

using modified ben’s syntax:

[:and
 [:map
  [:a :any]
  [:b :any]
  [:c :any]
  [:d :any]]
 [:rel
  [:or
   [:and :a :b]
   [:and :c :d]]]]

Ben Sless 2021-05-03T13:38:00.147600Z

The only objection I can come up with is that meander is too powerful, (i.e. the result will be hard to work with)

borkdude 2021-05-03T13:38:22.148Z

I think it can be confusing to overload the meaning of :or within :rel

☝️ 1
Ben Sless 2021-05-03T13:39:24.149300Z

"Here, have this fighter-jet fueled by liquid plutonium" Scary

😂 1
borkdude 2021-05-03T13:39:34.149800Z

Maybe just [:and [:map ....] [:or [:required-keys :a :b] [:required-keys :c :d]] [:gtk :a :b]]

borkdude 2021-05-03T13:39:42.150600Z

(gtk = greather than for keys, or something)

Ben Sless 2021-05-03T13:40:08.151400Z

The rel syntax needs to be properly thought out, I just invented something ad-hoc to see if it could work

borkdude 2021-05-03T13:40:14.151600Z

[:keys/> :a :b]

Ben Sless 2021-05-03T13:40:37.152100Z

How about :constraints?

borkdude 2021-05-03T13:40:43.152300Z

[:keys/require :a :b]

Ben Sless 2021-05-03T13:41:16.153Z

Then constraints unify by default (like datalog) or disjoin when specified (via :or)

borkdude 2021-05-03T13:41:17.153100Z

[:k> :a :b], [:kreq :a :b]

ikitommi 2021-05-03T13:42:01.153300Z

this works already:

(m/validate
  [:schema {:registry {::a :int
                       ::b :int
                       ::c :int
                       ::d :int}}
   [:or
    [:map ::a ::b]
    [:map ::c ::d]]] 
  {::a 1, ::b 2})
; => true

borkdude 2021-05-03T13:42:25.153900Z

yeah, that's what I said in the start of the conversation: two disjunct map defs

ikitommi 2021-05-03T13:42:33.154300Z

just not with non-qualified keys (for no good reason)

Ben Sless 2021-05-03T13:42:42.154800Z

[:constraints
 [:or
  [:and
   [:requires :a :b]
   [:> :a :b]]
  [:and
   [:requires :c :d]
   [:< :c :d]]]]

borkdude 2021-05-03T13:42:54.155100Z

but for rels between keys you could just have specialized ops like [:k> :a :b]: the :a key must be greater than the :b key, without introducing some new concept

1
ikitommi 2021-05-03T13:43:46.155800Z

i kinda like it.

Ben Sless 2021-05-03T13:44:18.156700Z

special ops rub me the wrong way, for some reason. What's wrong with just :>?

ikitommi 2021-05-03T13:44:34.157100Z

but question is: from whom the declaration are for? schema writer? user? external docs?

ikitommi 2021-05-03T13:45:13.157800Z

(m/validate [:> 6] 4)
; => false

ikitommi 2021-05-03T13:45:19.158Z

it already exists

Yehonathan Sharvit 2021-05-03T13:45:40.158100Z

Is there an answer to this in TypeScript?

Ben Sless 2021-05-03T13:46:23.158900Z

hm, yes, but as a schema not an argument (if we go back to the [:rel x y z] or [:constraint ,,,] suggestion

borkdude 2021-05-03T13:47:24.159400Z

> What's wrong with just :> That it means different things in different contexts, this can be confusing imo.

borkdude 2021-05-03T13:48:49.159900Z

What if you want to really do numeric comparison like [:> :a 5] and 5 is also a key in a map?

borkdude 2021-05-03T13:49:03.160300Z

In jet I chose [:> :a #jet/lit 5]

borkdude 2021-05-03T13:49:13.160600Z

but quickly this became tedious so I wrote a clojure interpreter

ikitommi 2021-05-03T13:49:13.160700Z

I think you have to use union types with ts

Yehonathan Sharvit 2021-05-03T13:49:29.160900Z

But it doesn’t scale. When you have other set of constraints you’d need to write down all the combinations of valid keys.

Ben Sless 2021-05-03T13:50:37.161900Z

The most shorthand constraint syntax I can think of would be :! An alternative which conforms to your suggestion would be :!/>

Ben Sless 2021-05-03T13:51:51.162800Z

Downside - it looks like Perl Upside - concise and unique syntax

🚀 1
borkdude 2021-05-03T13:52:37.163400Z

you should always make a good trade-off between adding extra syntax (= complexity) and how much people are really going to use this

☝️ 2
ikitommi 2021-05-03T13:53:14.164100Z

[:and
 [:map
  [:a :any]
  [:b :any]
  [:c :any]
  [:d :any]]
 [:keys/or
  [:keys/and :a :b]
  [:keys/and :c :d]]]

borkdude 2021-05-03T13:53:26.164500Z

yeah, I like that a lot better

borkdude 2021-05-03T13:54:05.165500Z

why even :keys/or, you can just use the normal :or here?

Yehonathan Sharvit 2021-05-03T13:54:28.166500Z

I was about to write the same thing

Ben Sless 2021-05-03T13:54:37.166900Z

Then adding a predicate function on the keys would look like

[:and
 [:map
  [:a :any]
  [:b :any]
  [:c :any]
  [:d :any]]
 [:or
  [:and
   [:keys/and :a :b]
   [:keys/< :a :b]]
  [:keys/and :c :d]]]

Yehonathan Sharvit 2021-05-03T13:54:41.167100Z

The disjunction must not be about the keys

ikitommi 2021-05-03T13:55:32.168100Z

true

Ben Sless 2021-05-03T13:55:32.168300Z

:keys/or should mean "at least k1 or k2 should exist"

borkdude 2021-05-03T13:55:59.169100Z

what does :keys/and mean: both keys must be present? what does [:keys/and :a] mean then? Maybe [:keys/req :a] is a better name?

1
Yehonathan Sharvit 2021-05-03T13:56:12.169200Z

And now, where would we put a custom predicate [:fn …]?

Ben Sless 2021-05-03T13:56:47.169600Z

Maybe even without a shorthand, keys/require

borkdude 2021-05-03T13:57:02.170200Z

or :keys/with and :keys/without ;P

👀 1
Yehonathan Sharvit 2021-05-03T13:57:04.170300Z

:keys/req doesn’t convey the fact that :a and :b are related

ikitommi 2021-05-03T13:57:06.170500Z

off to cook some food. good discussion, as always 👍

borkdude 2021-05-03T13:57:35.170600Z

but what exactly does [:keys/and :a :b] mean then, how are these things related?

Ben Sless 2021-05-03T13:58:18.170800Z

Thank you, feel like we hit on something useful and needed Bon appetit!

Yehonathan Sharvit 2021-05-03T13:58:48.171Z

I didn’t say I liked :keys/and

Yehonathan Sharvit 2021-05-03T13:59:16.171200Z

Maybe [:keys/present :a :b]?

Yehonathan Sharvit 2021-05-03T14:00:11.171700Z

By the way, my use case is real for a project at work.

borkdude 2021-05-03T14:00:12.171800Z

but you also didn't say what the semantic relationship between those keys is according to that op. > :keys/req doesn’t convey the fact that :a and :b are related :present also doesn't communicate a relationship, just as :required doesn't, it just means all those keys must be required/present

Ben Sless 2021-05-03T14:00:16.172300Z

same

Yehonathan Sharvit 2021-05-03T14:00:29.172800Z

We are writing a Hbase driver

borkdude 2021-05-03T14:00:58.173900Z

I guess you can already express required keys like this as well? [:and [:map ...] :a :b :c] or do keywords not behave like predicates in malli?

Yehonathan Sharvit 2021-05-03T14:01:30.174800Z

The scan function receives a map that could contain either :from and :to or :starts-with

Ben Sless 2021-05-03T14:01:45.175100Z

(m/schema [:and :a :b])
exception

borkdude 2021-05-03T14:01:46.175300Z

It looks like malli is becoming really popular in Israel?

borkdude 2021-05-03T14:02:13.175800Z

ok, [:and [:fn :a] [:fn :b]] ? :thinking_face:

borkdude 2021-05-03T14:02:37.176300Z

(ugly)

Yehonathan Sharvit 2021-05-03T14:02:40.176600Z

There are not so many Clojure shops i Israel. Therefore, we could already say that 10% of Israeli Clojure shops use malli 😁

Ben Sless 2021-05-03T14:03:14.176700Z

And non informative, and useless for generation

(me/humanize (m/explain (m/schema [:and [:fn :a]]) {:b 1}))
;; => #:malli{:error ["unknown error"]}

borkdude 2021-05-03T14:03:32.177200Z

agreed

Yehonathan Sharvit 2021-05-03T14:06:16.180600Z

I’m struggling to write the schema of the map received by scan

[:map    
   [:starts-with :string]
   [:from :string]
   [:to :string]
 [:xor 
   [:keys/req :starts-with]
   [:keys/req :from :to]
   [:keys/req :from]
   [:keys/req :to]
 ]
]

Yehonathan Sharvit 2021-05-03T14:06:44.181200Z

:or is not good in my case. It should be exclusive or

borkdude 2021-05-03T14:07:12.182100Z

if it is xor, then why not use disjunct maps?

Yehonathan Sharvit 2021-05-03T14:07:26.182400Z

Because there are other keys

borkdude 2021-05-03T14:07:42.182900Z

maybe add a :type field or something?

Yehonathan Sharvit 2021-05-03T14:07:59.183200Z

:time-range , :limit

Yehonathan Sharvit 2021-05-03T14:08:45.184100Z

[:map    
   [:starts-with :string]
   [:from :string]
   [:to :string]
   [:limit :number]
   [:time-range [:map [:from-ms :int] [:to-ms :int]]
 [:xor 
   [:keys/req :starts-with]
   [:keys/req :from :to]
   [:keys/req :from]
   [:keys/req :to]
 ]
]

Yehonathan Sharvit 2021-05-03T14:08:59.184400Z

What is a :type field?

borkdude 2021-05-03T14:09:57.185Z

I think you could have different types of ranges

borkdude 2021-05-03T14:11:03.185800Z

{:range-type :closed}, {:range-type :open}

Ben Sless 2021-05-03T14:11:09.185900Z

In this case I'd start with some base schema, merge the different options with it and or between them

Yehonathan Sharvit 2021-05-03T14:11:23.186Z

I was wrong. There is no relationship between the keys. But somehow I find it weird to say “required” inside an or condition

Yehonathan Sharvit 2021-05-03T14:11:44.186200Z

Does it help to define constraints on map keys

Yehonathan Sharvit 2021-05-03T14:11:46.186400Z

?

Yehonathan Sharvit 2021-05-03T14:12:45.186800Z

I don’t get what both of you suggested

borkdude 2021-05-03T14:17:19.188900Z

@viebel I think this requirement is a bit weird:

:xor
[:keys/req :from :to]
[:keys/req :from]
if from is present, and to is not, then the second is true. but if to is present, then the first one is true. so to is optional.

borkdude 2021-05-03T14:17:50.189300Z

I did not look at the other requirements, but I think your logic can be "refactored"

borkdude 2021-05-03T14:17:56.189500Z

by modeling it differently

emccue 2021-05-03T14:22:36.190500Z

hmm, this is a tricky one

emccue 2021-05-03T14:23:56.191700Z

[:or [:map {:closed true}
       [:from ...]
       [:to   ...]]
     [:map {:closed true}
       [:starts-with ...]]]

emccue 2021-05-03T14:24:19.192200Z

OR is an exclusive or if the types don't intersect

emccue 2021-05-03T14:24:36.192800Z

[:map 
  [:from ...]
  [:to   ...]]

emccue 2021-05-03T14:24:51.193200Z

but if you have it closed you can't have anything other than from and to

emccue 2021-05-03T14:25:29.193900Z

and if you have it open you leave open the possibility that :starts-with is in the map as well

emccue 2021-05-03T14:25:41.194200Z

what you need is basically

emccue 2021-05-03T14:27:08.195900Z

[:or [:and [:map 
             [:from ...]
             [:to ...]]
           [:not [:map 
                   [:starts-with ...]]]]
     [:and [:map 
             [:starts-with ...]]
           [:not [:map 
                   [:from ...]
                   [:to   ...]]]]

emccue 2021-05-03T14:27:14.196100Z

OR

emccue 2021-05-03T14:27:50.196900Z

[:or [:and [:map 
             [:from ...]
             [:to ...]]
           [:not [:map 
                   [:starts-with ...]]]]
     [:and [:map 
             [:starts-with ...]]
           [:not [:map [:from ...]]
           [:not [:map [:to ...]]]]]

emccue 2021-05-03T14:28:27.197400Z

not that not exists, but conceptually thats what you would need to represent the constraint

emccue 2021-05-03T14:28:54.197800Z

since you only want to be closed against specific keys

emccue 2021-05-03T14:29:23.198300Z

if you are willing to explicitly enumerate all the keys you can just do the version with {:closed

Ben Sless 2021-05-03T14:29:52.198500Z

I meant something like

(def Base
  [:map
   [:limit :number]
   [:time-range [:map [:from-ms :int] [:to-ms :int]]]])

(def S1 (mu/merge Base [:map [:starts-with :string]]))
(def S2 (mu/merge Base [:map [:from :string] [:to :string]]))

Ben Sless 2021-05-03T15:02:30.198900Z

Did some generalizing work, still not set on the syntax

(defn comparator-relation
  [sym msg]
  (let [f @(resolve sym)
        type (keyword "!" (name sym))]
    [type
     (m/-simple-schema
      (fn [_ [a b]]
        (let [fa #(get % a)
              fb #(get % b)]
          {:type type
           :pred (m/-safe-pred #(f (fa %) (fb %))),
           :type-properties
           {:error/fn
            {:en (fn [{:keys [schema value]} _]
                   (str
                    "value at key "
                    a ", "
                    (fa value)
                    ", should be "
                    msg
                    " value at key "
                    b
                    ", "
                    (fb value)))}}
           :min 2,
           :max 2})))]))

(defn -comparator-relation-schemas
  []
  (into
   {}
   (map (fn [[sym msg]] (comparator-relation sym msg)))
   [['>  "greater than"]
    ['>= "greater than or equal to"]
    ['= "equal to"]
    ['== "equal to"]
    ['<= "lesser than or equal to"]
    ['< "lesser than"]]))


(me/humanize
 (m/explain
  (m/schema
   [:and
    [:map
     [:x :int]
     [:y :int]]
    [:!/> :x :y]]
   {:registry
    (mr/composite-registry
     m/default-registry
     (-comparator-relation-schemas))})
  {:x 1 :y 1}))

Yehonathan Sharvit 2021-05-03T15:28:28.199Z

I don’t see how to refactor the logic. All the followings are valid • :from 10 :to 20 • :form 10 • :to 20 • :starts-with aaa

Yehonathan Sharvit 2021-05-03T15:28:54.199300Z

The followings are invalid

Yehonathan Sharvit 2021-05-03T15:29:29.199500Z

• :from 10 :starts-with aaa • :to 20 :starts-with aaa • “from 10 :to 20 starts-with aaa

Joel 2021-05-03T17:39:29.200100Z

I'm surprised that TypeScript is sophisticated enough to be used as inspiration, I guess I need to learn more. I've presumed Elm/Haskell are more sophisticated, eg.: https://www.youtube.com/watch?v=IcgmSRJHu_8