Records delegate their clojure.lang interface methods to the map fns

A defrecord is Associative/ILookup/IPersistentMap/Seqable/Counted on the JVM,
so (.assoc r k v) / (.valAt r k) / (.without r k) / (.containsKey r k) /
(.cons r x) / (.count r) / (.seq r) / (.equiv r o) / (.entryAt r k) now work
via Java interop, delegating to the map fns when not overridden by a declared
method. reitit's impl calls (.assoc match k v) directly. A bare deftype uses
its own declared methods (record-only branch). reitit-core 58/1/18 -> 321/1/1
with the router lib's Trie shim.
This commit is contained in:
Yogthos 2026-06-26 22:49:51 -04:00
parent 5cd8d15ae7
commit ed1ea46ca2
2 changed files with 23 additions and 0 deletions

View file

@ -728,6 +728,28 @@
;; (above) wins, this is the field-accessor fallback. ;; (above) wins, this is the field-accessor fallback.
((and (jrec? obj) (null? rest) (jrec-has? obj (keyword #f method-name))) ((and (jrec? obj) (null? rest) (jrec-has? obj (keyword #f method-name)))
(jrec-lookup obj (keyword #f method-name) jolt-nil)) (jrec-lookup obj (keyword #f method-name) jolt-nil))
;; a defrecord is Associative / ILookup / IPersistentMap / Seqable / Counted,
;; so its clojure.lang interface methods delegate to the map fns when not
;; overridden by a declared method — reitit's impl calls (.assoc match k v),
;; (.valAt …), (.without …) directly. A bare deftype implements these via its
;; own declared methods (handled above), so this is record-only.
((and (jrec-record? obj)
(member method-name '("valAt" "assoc" "without" "containsKey" "cons"
"count" "seq" "equiv" "entryAt" "empty")))
(cond
((string=? method-name "valAt")
(if (null? (cdr rest)) (jolt-get obj (car rest) jolt-nil) (jolt-get obj (car rest) (cadr rest))))
((string=? method-name "assoc") (jolt-assoc1 obj (car rest) (cadr rest)))
((string=? method-name "without") (jolt-dissoc obj (car rest)))
((string=? method-name "containsKey") (if (jolt-truthy? (jolt-contains? obj (car rest))) #t #f))
((string=? method-name "cons") (jolt-conj1 obj (car rest)))
((string=? method-name "count") (jolt-count obj))
((string=? method-name "seq") (jolt-seq obj))
((string=? method-name "equiv") (if (jolt= obj (car rest)) #t #f))
((string=? method-name "entryAt")
(if (jolt-truthy? (jolt-contains? obj (car rest)))
(make-map-entry (car rest) (jolt-get obj (car rest) jolt-nil)) jolt-nil))
(else jolt-nil))) ; .empty of a record is nil on the JVM
((reified-methods obj) ((reified-methods obj)
=> (lambda (rm) (let ((f (hashtable-ref rm method-name #f))) => (lambda (rm) (let ((f (hashtable-ref rm method-name #f)))
(if f (apply jolt-invoke f obj rest) (error #f (string-append "No method " method-name)))))) (if f (apply jolt-invoke f obj rest) (error #f (string-append "No method " method-name))))))

View file

@ -3300,4 +3300,5 @@
{:suite "defn / attr-map" :label "attr-map without a docstring" :expected "true" :actual "(do (defn g {:my :attr} [] 1) (= :attr (:my (meta (var g)))))"} {:suite "defn / attr-map" :label "attr-map without a docstring" :expected "true" :actual "(do (defn g {:my :attr} [] 1) (= :attr (:my (meta (var g)))))"}
{:suite "defn / attr-map" :label "attr-map on a multi-arity defn" :expected "true" :actual "(do (defn h \"d\" {:my :a} ([] 1) ([a] a)) (= :a (:my (meta (var h)))))"} {:suite "defn / attr-map" :label "attr-map on a multi-arity defn" :expected "true" :actual "(do (defn h \"d\" {:my :a} ([] 1) ([a] a)) (= :a (:my (meta (var h)))))"}
{:suite "defn / attr-map" :label "attr-map values are evaluated" :expected "true" :actual "(do (defn m {:val (+ 1 2)} [] 1) (= 3 (:val (meta (var m)))))"} {:suite "defn / attr-map" :label "attr-map values are evaluated" :expected "true" :actual "(do (defn m {:val (+ 1 2)} [] 1) (= 3 (:val (meta (var m)))))"}
{:suite "records / clojure.lang interop" :label "a record's Associative/ILookup methods delegate to the map fns" :expected "[1 9 true true 2]" :actual "(do (defrecord M [a b]) (let [m (->M 1 2)] [(.valAt m :a) (.valAt m :z 9) (= {:a 1 :b 2 :c 3} (into {} (.assoc m :c 3))) (.containsKey m :a) (.count m)]))"}
] ]