malli

sammy 2026-03-04T03:38:04.433279Z

is there a standard practice for adding (dev/start! {:report (pretty/thrower)}) to all REPL sessions of a project? I tried adding the following to the dir-locals for my project

((nil . ((cider-clojure-cli-aliases . "test")
(cider-repl-init-code . ("(when-let [requires (resolve 'clojure.main/repl-requires)]
(clojure.core/apply clojure.core/require @requires))"
"(require '[malli.dev :as dev] '[malli.dev.pretty :as pretty]) (dev/start! {:report (pretty/thrower)}) (println \"malli dev started\")")))))
and despite seeing
;;  Startup: /opt/homebrew/bin/clojure -Sdeps \{\:deps\ \{nrepl/nrepl\ \{\:mvn/version\ \"1.5.1\"\}\ cider/cider-nrepl\ \{\:mvn/version\ \"0.58.0\"\}\ refactor-nrepl/refactor-nrepl\ \{\:mvn/version\ \"3.11.0\"\}\}\ \:aliases\ \{\:cider/nrepl\ \{\:main-opts\ \[\"-m\"\ \"nrepl.cmdline\"\ \"--middleware\"\ \"\[refactor-nrepl.middleware/wrap-refactor\,cider.nrepl/cider-middleware\]\"\]\}\}\} -M:test:cider/nrepl
malli: dev-mode started
malli dev started
user> 
at the top of my REPL session instrumentation throws do not take effect, until I additionally add
(dev/start! {:report (pretty/thrower)})
in my repl. I also checked that, even before calling dev/start! manually (in the situation where no error is thrown)
(malli.core/function-schemas)
correctly contains the schema for the function I want to instrument, so it looks like it's known to the Malli environment. I think the next step to debug is to check what reporter is currently active, but I'm not sure where to access it -- i dug through the malli docs, and src/malli/instrument.clj, but couldn't quite figure it out. an advice? am i missing something very dumb?

sammy 2026-03-10T19:35:44.439259Z

that makes sense -- thanks for the detailed thoughts! currently the pattern of using

(defn my-test [a] "hi")
(m/=> my-test [:=> [:cat :int] :string])
is working well enough for me that I'm going to keep using it, but ill look into mx/defn as well

sammy 2026-03-06T23:19:25.282759Z

The specific thing that confused me is, the sequence of calling:

(dev/start! {:report (pretty/thrower)})
;; load the following namespace
(ns mystuff)
(defn foo [x] "invalid")
(m/=> [:=> [:cat :int] :int])
;; then from somewhere else call
(foo "hi!")
results in different behavior from the sequence of calling:
(dev/start! {:report (pretty/thrower)})
;; load the following namespace
(ns mystuff)
;; different ordering of m/=>
(m/=> [:=> [:cat :int] :int])
(defn foo [x] "invalid")
;; then from somewhere else call
(foo "hi!")
maybe it was never the intention for sequence #2 to be well-defined behavior, but it surprised me that they created different results. (fwiw i wanted to do sequence 2 because I wanted the behavior of malli throws globally for every function that is annotated regardless of when/how it was imported)

opqdonut 2026-03-06T06:09:49.847529Z

Re: ordering of defn and =>, the malli.demo example still works for me.

% clj -M:test
Clojure 1.12.3
user=> (require 'malli.demo)
nil
user=> (in-ns 'malli.demo)
#object[clojure.lang.Namespace 0x1dc37d4f "malli.demo"]
malli.demo=> (dev/start!)
malli: instrumented 3 function vars
malli: dev-mode started
nil
malli.demo=> (kikka "1")
-- Invalid Function Input ------------------------------------------------------

Invalid function arguments

  ["1"]

Function Var

  malli.demo/kikka

Input Schema

  [:cat :int]

Errors

  {:in [0], :message "should be an integer", :path [0], :schema :int, :value "1"}

More information

  

--------------------------------------------------------------------------------
Execution error (ClassCastException) at malli.demo/kikka (demo.cljc:9).
class java.lang.String cannot be cast to class java.lang.Number (java.lang.String and java.lang.Number are in module java.base of loader 'bootstrap')

malli.demo=> (kukka "1")
-- Invalid Function Input ------------------------------------------------------

Invalid function arguments

  ["1"]

Function Var

  malli.demo/kukka

Input Schema

  [:cat :int]

Errors

  {:in [0], :message "should be an integer", :path [0], :schema :int, :value "1"}

More information

  

--------------------------------------------------------------------------------
Execution error (ClassCastException) at malli.demo/kukka (demo.cljc:14).
class java.lang.String cannot be cast to class java.lang.Number (java.lang.String and java.lang.Number are in module java.base of loader 'bootstrap')
malli.demo=>

opqdonut 2026-03-06T06:12:04.984799Z

It doesn't work if I do ! before requiring malli.demo. That's because start! only looks at namespaces that have already been loaded. It doesn't hook new namespace loading.

opqdonut 2026-03-06T06:12:27.496389Z

This is as documented in the start! docstring.

opqdonut 2026-03-06T06:12:41.855019Z

Would some documentation improvements help you here?

opqdonut 2026-03-09T13:27:17.542529Z

yeah I'm not sure how sequence #2 actually does anything, I'll need to recheck the implementation

opqdonut 2026-03-09T13:29:51.803869Z

Riiiight, start! adds a watch on the -function-schemas* atom that does instrument!, and m/=> puts stuff in that atom, so that's why defn + m/=> works (the defn is in place to be instrumented), and m/=> + defn doesn't.

opqdonut 2026-03-09T13:30:37.231129Z

For a usecase like this, maybe mx/defn is more ergonomic? There's definitely a footgun here that could be documented better.

sammy 2026-03-04T13:09:07.358429Z

ok, so it turns out that this happened because I reversed the order of the malli schema defs i.e. this does not lint:

(m/=> my-test [:=> [:cat :int] :string])
(defn my-test [a] "hi")
but this does:
(defn my-test [a] "hi")
(m/=> my-test [:=> [:cat :int] :string])
this was really non-obvious to me because the header image uses the old style, and this snippet from the docs implies that both orders should have the same behavior

2026-03-04T20:01:50.999249Z

I would recommend adding this to your dev/user.clj file in a function, and then calling it when you start your repl:

(defn malli-start []
  (require '[malli.dev :as dev]
           '[malli.dev.pretty :as pretty])
  (dev/start! {:report (pretty/thrower)})
  (println \"malli dev started\"))

✅ 1
2026-03-04T20:02:33.970809Z

i don't know if that solves the issue for the order of malli schema defs vs actual defs, i didn't read your reply befoer writing that lol

2026-03-04T20:02:54.633589Z

but the way that user.clj is loaded, i think it's probably safest to wait until everything else is loaded before starting instrumentation