Skip to content

Instantly share code, notes, and snippets.

@jarppe
Last active August 26, 2025 11:31
Show Gist options
  • Select an option

  • Save jarppe/734edcaba0ffbf9b912972dd222b8ac1 to your computer and use it in GitHub Desktop.

Select an option

Save jarppe/734edcaba0ffbf9b912972dd222b8ac1 to your computer and use it in GitHub Desktop.
clojure function serialization and deserialization example
(ns jarppe.fn-ser.core
(:require [clojure.walk :as walk])
(:import (clojure.lang Namespace
Var)))
(set! *warn-on-reflection* true)
(defmacro defn+
"Like `clojure.core/defn` but saves form and other information in
function metadata. Contrast this to core `defn` which puts all
metadata into the var. This allows obtaining this information for
serialization even when code is aot compiled using direct linking.
Notable differences to core `defn`:
* `defn+` saves the function form in metadata
* `defn+` saves the metadata to the function, not to the var
* `defn+` does not support `prepost-map`, if one is present raises an exception
* `defn+` does not support the trailing attr-map, if one is present it will be ignored
* if function body has symbols that shadow symbols from ns the results will be undefined
The last bullet means that if your ns has mapping like:
```
(ns my.ns
(:require [clojure.string :refer [join]]))
```
and your function shadows this `join` like this:
```
(defn+ my-fn [x]
(let [join (+ x 2)]
join))
```
then the `defn+` will fail. The `defn+` needs to convert all symbols that refer external
functions or classes to fully qualified symbols, and in the above example, it converts the
`join` inside the function body into `clojure.string/join`."
{:arglists '([name doc-string? attr-map? [params*] body]
[name doc-string? attr-map? ([params*] body) +])}
[fn-name & more]
(let [[doc-string & more] (if (-> more (first) (string?))
more
(cons nil more))
[attr-map & form] (if (-> more (first) (map?))
more
(cons nil more))
;; Check that the prepost-map is not used:
_ (when (cond
;; single arity variant
(-> form (first) (vector?)) (-> form (second) (map?))
;; multi arity variant
(-> form (first) (list?)) (some (fn [f] (-> f (second) (map?))) form))
(throw (ex-info "defn+ does not support prepost-map" {})))
reqs (volatile! #{})
fq-form (walk/prewalk (fn [v]
(if-let [x (and (symbol? v) (ns-resolve *ns* v))]
(cond
(var? x) (let [fqs (Var/.toSymbol x)]
(vswap! reqs conj (-> fqs (namespace) (symbol)))
fqs)
(class? x) (-> x (Class/.getName) (symbol))
:else x)
v))
form)
meta-data (merge (when doc-string {:doc doc-string})
attr-map
(meta &form)
{:defn+ true
:fn-name (list `quote fn-name)
:file *file*
:ns (list `quote (Namespace/.getName *ns*))
:form (list `quote (list `fn fn-name fq-form))
:reqs (->> (disj @reqs 'clojure.core)
(mapv (fn [rns] (list `quote rns)))
(list `set))})]
(list `def fn-name (list `vary-meta (list* `fn fn-name fq-form)
`merge meta-data))))
(comment
(defn+ foo [a b] (+ a b))
(foo 1 2)
;;=> 3
(meta foo)
;;=> {:line 53
;; :column 3
;; :defn+ true
;; :fn-name foo
;; :file "/Users/jarppe/swd/cc/misc/fn-ser/src/main/jarppe/fn_ser/core.clj"
;; :ns jarppe.fn-ser.core
;; :form (fn foo ([a b] (clojure.core/+ a b)))
;; :reqs #{}}
(defn plus [a b] (+ a b))
(defn+ foo [a b] (plus a b))
(foo 1 2)
;;=> 3
(meta foo)
;;=> {:line 66
;; :column 3
;; :defn+ true
;; :fn-name foo
;; :file "/Users/jarppe/swd/cc/misc/fn-ser/src/main/jarppe/fn_ser/core.clj"
;; :ns jarppe.fn-ser.core
;; :form (fn foo ([a b] (jarppe.fn-ser.core/plus a b)))
;; :reqs #{jarppe.fn-ser.core}}
(require '[clojure.string :as str])
(defn+ foo [a b] (str/join (str/upper-case a) b))
(foo "-foo-" ["a" "b" "c"])
;;=> "a-FOO-b-FOO-c"
(meta foo)
;;=> {:line 78
;; :column 3
;; :defn+ true
;; :fn-name foo
;; :file "/Users/jarppe/swd/cc/misc/fn-ser/src/main/jarppe/fn_ser/core.clj"
;; :ns jarppe.fn-ser.core
;; :form (fn foo ([a b] (clojure.string/join (clojure.string/upper-case a) b)))
;; :reqs #{clojure.string}}
(defn+ foo "fancy func" {:foo "bar"}
([a] (str a))
([a b] (str a b)))
(foo 1)
(foo 1 2)
(meta foo)
;;=> {:doc "fancy func"
;; :foo "bar"
;; :line 89
;; :column 3
;; :defn+ true
;; :fn-name foo
;; :file "/Users/jarppe/swd/cc/misc/fn-ser/src/main/jarppe/fn_ser/core.clj"
;; :ns jarppe.fn-ser.core
;; :form (fn foo (([a] (clojure.core/str a)) ([a b] (clojure.core/str a b))))
;; :reqs #{}}
)
(defn serialize [f]
(assert (-> f (fn?)) "arg must be a fn")
(assert (-> f (meta) :defn+) "arg must be defined using `defn+` macro")
(select-keys (meta f) [:form :reqs]))
(defn deserialize [{:keys [form reqs]}]
(doseq [req-ns reqs]
(require req-ns))
(eval form))
(comment
(require '[clojure.java.io :as io]
'[clojure.string :as str])
(defn+ hello
"example function"
[file-name]
(println (str/upper-case "Here we go again..."))
(println "file size:" (-> (io/file file-name) (.length))))
(hello "./deps.edn")
; prints:
; HERE WE GO AGAIN...
; file size: 561
(serialize hello)
;;=> {:form (clojure.core/fn hello
;; ([file-name]
;; {:pre (clojure.core/string? file-name)}
;; (clojure.core/println (clojure.string/upper-case "Here we go again..."))
;; (clojure.core/println "file size:" (clojure.core/-> (clojure.java.io/file file-name) (.length)))))
;; :reqs #{clojure.java.io
;; clojure.string}}
(def hello-2 (deserialize (serialize hello)))
(hello-2 "./deps.edn")
; prints:
; HERE WE GO AGAIN...
; file size: 561
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment