Last active
August 26, 2025 11:31
-
-
Save jarppe/734edcaba0ffbf9b912972dd222b8ac1 to your computer and use it in GitHub Desktop.
clojure function serialization and deserialization example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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