Liking cljdoc? Tell your friends :D

malt

Typed Clojure protocols using Malli schemas.

Clojars
Project


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:

  • Defining more concrete interfaces between systems that are self-documented
  • Asserting that data flowing over system boundaries conform to the defined interface
  • Generating services for different networking protocols (GRPC, HTTP, other)
  • Generating SDK clients from well-defined interfaces
  • Generating openapi schemas from interfaces

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:

  1. It should be easy to read and write, requiring minimal cognitive overhead to interpret
  2. It needs to produce a native Clojure protocol, supporting all the related features:
  • Reification
  • Type Extension
  • Go-To-Definition
  • Go-To-Implementations
  • etc.
  1. It should allow runtime input/output validation using the schemas defined on the protocol
  2. It needs to work flawlessly in my editor, requiring no special wrangling. My editor needs deep understanding of the syntax, which goes back to point 1. of enabling features like go-to-definition.

All the above is possible to achieve using some custom macros and clj-kondo to wire it all back into the editor/clojure-lsp.

Sneak Peak

(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://..."})})

;; Some top-level malli schemas are also exported as vars which can be used to validate
;; record types.
(m/validate ?Service (->Service db))
(m/validate ?ServiceSchema {:db db})

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close