warning
This is just an experiment right now. If I like the outcome then I may take it further and the interfaces and APIs will almost certainly change.
Don't read the code, it was generated by AI. I just defined the interfaces and behaviour I wanted and got AI to generate the macros and clj-kondo hooks. I will rewrite it all if I decide to take this further.
This started out as an experiment to see if it was possible to combine Malli schemas with Clojure protocols, and the results were promising enough to turn into what you see here.
I wanted the ability to define concrete, typed interfaces in Clojure that don't suffer from bit-rot and that can leverage the Malli schema ecosystem. These are the kind of things I wanted to use these definitions for:
And there are, I am sure, many more things you can do with this interface combo.
There are some important properties I need to get out of this for it to be something I want to use every day and extensively throughout a large codebase:
All the above is possible to achieve using some custom macros and clj-kondo to wire it all back into the editor/clojure-lsp.
(ns example
(:require
[io.julienvincent.malt :as malt]))
(def ?Plumburg
[:map
[:name :string]
[:edges :int]])
;; Define a protocol, substituting the parameters with malli schemas
(malt/defprotocol Api
(create-plumburg [name :string edges :int] ?Plumburg))
(defrecord Service [db])
;; The `malt/extend` API works like `clojure.core/extend-type` but adds
;; malli schema validation to the input arguments and returned result.
(malt/extend Service
Api
(create-plumburg [service name edges]
(write-to-db (:db service) name edges)))
(create-plumburge (Service. db) "fred" "2") ;; Failed with a validation exception
;; You can use `malt/implement` which works like `clojure.core/reify` but with validation
(defn create-service [db]
(malt/implement Api
(create-plumburg [_ name edges]
(write-to-db db name edges))))
;; Because `malt/defprotocol` produces a real clojure protocol, you can still use reify
(defn create-service-with-reify [db]
(reify Api
(create-plumburg [_ name edges]
(write-to-db db name edges))))
;; Likewise for things like `extend-type`. They still work, just without the runtime validation
(extend-type Service
Api
(create-plumburg [service name edges]
(write-to-db (:db service) name edges)))
;; We can also improve on the schemaless record type defined above:
(def ?DataSource
[:fn {:error/message "Must be an instance of javax.sql.DataSource"}
(fn [value]
(instance? javax.sql.DataSource value))])
;; Using `malt/defrecord` works identically to `clojure.core/defrecord` but overrides the
;; generated `->Type` and `map->Type` constructors to add schema validation.
(malt/defrecord Service
[db ?DataSource])
;; Fails with a validation error
(map->DataSource {:db 1})
(->DataSource "Not a DataSource")
;; Success!
(map->DataSource {:db (jdbc/get-datasource {:uri "postgres://..."})})
See the test for some more examples of how it works
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |