malli icon indicating copy to clipboard operation
malli copied to clipboard

mx/defn and optional named arguments?

Open iterati opened this issue 1 year ago • 4 comments

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.

iterati avatar Jan 30 '24 18:01 iterati

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 avatar Jan 31 '24 12:01 larkery

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.

larkery avatar Jan 31 '24 12:01 larkery

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

iterati avatar Jan 31 '24 13:01 iterati

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]]]}

ikitommi avatar Feb 04 '24 08:02 ikitommi