metosin / malli

High-performance data-driven data specification library for Clojure/Script.
Eclipse Public License 2.0
1.44k stars 204 forks source link

mx/defn and optional named arguments? #1003

Open iterati opened 5 months ago

iterati commented 5 months ago

I'm attempting to spec a function where I destructure named arguments with defaults. I'm not sure where I'm going wrong here:

(mx/defn init! :- :map
  [& {:keys [level :- :keyword
             appenders :- :keyword
             output-fn :- :function]
      :or   {level     :info
             appenders core/default-appenders
             output-fn output-json-fn}}]
  (core/init! level appenders output-fn))

(mi/instrument!)

(init! 1) ;; => clojure.lang.ExceptionInfo as expected

(init! :level 1) ;; => {:min-level 1 ..}, didn't validate the input

(md/infer #'init!) ;; => :any for all types?
;; [:=>
 ;; [:cat
 ;;  [:altn
 ;;   [:map
 ;;    [:map
 ;;     [:level {:optional true} :any]
 ;;     [:- {:optional true} :any]
 ;;     [:keyword? {:optional true} :any]
 ;;     [:appenders {:optional true} :any]
 ;;     [:map? {:optional true} :any]
 ;;     [:output-fn {:optional true} :any]
 ;;     [:fn? {:optional true} :any]]]
 ;;   [:args
 ;;    [:*
 ;;     [:alt
 ;;      [:cat [:= :level] :any]
 ;;      [:cat [:= :-] :any]
 ;;      [:cat [:= :keyword?] :any]
 ;;      [:cat [:= :appenders] :any]
 ;;      [:cat [:= :map?] :any]
 ;;      [:cat [:= :output-fn] :any]
 ;;      [:cat [:= :fn?] :any]
 ;;      [:cat :any :any]]]]]]
 ;; :any]

I'm new to malli, so it might be something obvious I'm missing.

larkery commented 5 months ago

I have just literally tried the same thing; md/parse does not handle this case as far as I can tell.

(require '[malli.destructure :as md])
(md/parse '[& {:keys [a :- :int]}])
  ;; => {:raw-arglist [& {:keys [a :- :int]}],
  ;;     :parsed {:elems [], :rest {:amp &, :arg {:arg [:map {:keys [a :- :int]}]}}},
  ;;     :arglist [& {:keys [a :- :int]}],
  ;;     :schema
  ;;     [:cat
  ;;      [:altn
  ;;       [:map
  ;;        [:map
  ;;         [:a {:optional true} :any]
  ;;         [:- {:optional true} :any]
  ;;         [:int {:optional true} :any]]]
  ;;       [:args
  ;;        [:*
  ;;         [:alt
  ;;          [:cat [:= :a] :any]
  ;;          [:cat [:= :-] :any]
  ;;          [:cat [:= :int] :any]
  ;;          [:cat :any :any]]]]]]}

  (md/parse '[& {:keys [a]} :- [:map [:a :int]]])
  ;; => {:raw-arglist [& {:keys [a]} :- [:map [:a :int]]],
  ;;     :parsed
  ;;     {:elems [],
  ;;      :rest {:amp &, :arg {:arg [:map {:keys [a]}], :- :-, :schema [:map [:a :int]]}}},
  ;;     :arglist [& {:keys [a]}],
  ;;     :schema [:cat [:map [:a :int]]]}

The second case is roughly what we want the first case to do I believe.

larkery commented 5 months ago

Actually the second case also doesn't work, because the instrumented function is expected to have arity 1 rather than arity 2. So unless there is another syntax to use I don't think mx/defn can do this right now.

iterati commented 5 months ago

Thank you @larkery for the speedy answer. I'll move away from the named arguments style and pass in an opt map.

ikitommi commented 5 months ago

There is no support for defining types for destructured keys atm. The Schematize Syntax is taken from Plumatic Schema as Cursive understands that too.

Support for this would be simple to add but would add ambiquity issue, e.g. the following (valid clojure!) would not work:

(let [{:keys [a :- :int]} {:a 1, :- 2, :int 3}]
  [a - int])
; => [1 2 3]

So, this does not work:

(md/parse ['{:keys [a :- :int]}])
;{:raw-arglist [{:keys [a :- :int]}],
; :parsed {:elems [{:arg [:map {:keys [a :- :int]}]}], :rest nil},
; :arglist [{:keys [a :- :int]}],
; :schema [:cat
;          [:altn
;           [:map [:map [:a {:optional true} :any] [:- {:optional true} :any] [:int {:optional true} :any]]]
;           [:args [:schema [:* [:alt [:cat [:= :a] :any] [:cat [:= :-] :any] [:cat [:= :int] :any] [:cat :any :any]]]]]]]}

But this does:

(md/parse ['{:keys [a]} :- [:map [:a :int]]])
;{:raw-arglist [{:keys [a]} :- [:map [:a :int]]],
; :parsed {:elems [{:arg [:map {:keys [a]}], :- :-, :schema [:map [:a :int]]}], :rest nil},
; :arglist [{:keys [a]}],
; :schema [:cat [:map [:a :int]]]}

IMO Clojure should add support for optional type definitions, via plain : like TC39 is for JavaScript. With that:

(md/parse ['{:keys [a : :int]}])
;{:raw-arglist [{:keys [a : :int]}],
; :parsed {:elems [{:arg [:map {:keys [a]}], EMPTYKW EMPTYKW, :schema [:map [:a :int]]}], :rest nil},
; :arglist [{:keys [a : :int]}],
; :schema [:cat [:map [:a :int]]]}