Skip to content

Instantly share code, notes, and snippets.

@simon-brooke
Created May 27, 2024 12:54
Show Gist options
  • Save simon-brooke/5896998720be3efda3bfab9cfc182b2c to your computer and use it in GitHub Desktop.
Save simon-brooke/5896998720be3efda3bfab9cfc182b2c to your computer and use it in GitHub Desktop.
Getting multiple properties from a Java object: or, how not to do it.
(ns getstarstar
(:require [camel-snake-kebab.core :refer [->camelCase ->kebab-case-keyword]])
(:import [clojure.lang Keyword]
[java.io File]))
;; What this is about: Ertuğrul Çetin's [jme-clj](https://github.com/ertugrulcetin/jme-clj)
;; contains a very elegant `get*` macro which constructs the name of a java
;; gatter instance method on the fly and then calls that method. This allows you to get
;; an arbitrary instance value from a java 'bean'. It's very neat. But sometimes
;; you will want to get many instance values from the same object, and to return them as
;; a map.
;;
;; I wanted something like the clojure [`select-keys`]() function, which, given an object
;; and a set of keys, returned that set of keys and their values from the object as a map.
;; That's proven interestingly difficult.
;; First, Ertuğrul Çetin's implementation. This works:
(defmacro get*
"Java interop for methods with `get` prefix.
e.g.: (get* (cam) :rotation) -> (.getRotation (cam))"
[obj kw & args]
`(~(symbol (->camelCase (str ".get-" (name kw)))) ~obj ~@args))
(get* (File. "/etc/hostname") :absolute-path)
;; OK, it would be really nice, if the getter method doesn't exist, just to
;; return `nil` rather than blowing up the world. This should be easy, no?
;; It (now) also works
(defmacro try-get*
"Try to get the property whose name corresponds to this keyword `p` from
object `o`, but if it doesn't have such a property, return `nil`. `args`,
if passed, are additional arguments to be passed to the getter."
[^Object o ^Keyword p & args]
`(try
(~(symbol (->camelCase (str ".get-" (name p)))) ~o ~@args)
(catch IllegalArgumentException e#
;; (println (.getMessage e#))
nil)))
(try-get* (File. "/etc/hostname") :absolute-path)
;; => "/etc/hostname"
(try-get* (File. "/etc/hostname") :froboz)
;; No matching field found: getFroboz for class java.io.File
;; => nil
;; First attempt: function, calling `try-get*`. Should be straightforward, no?
(defn get**1
[^Object o & properties]
(reduce (fn [m p]
(println (format "m => '%s'; p => '%s'" m p))
(assoc m p (try-get* o p))) {} (filter keyword? properties)))
(get**1 (File. "/etc/hostname") :name :absolute-path :froboz)
;; This feels bizarre. What gets captured as the second argument to the anonymous
;; function in `reduce` is literal `p`, not what the binding of `p` is within
;; `get**`:
;; m => '{}'; p => ':name'
;; No matching field found: getP for class java.io.File
;; m => '{:name nil}'; p => ':absolute-path'
;; No matching field found: getP for class java.io.File
;; m => '{:name nil, :absolute-path nil}'; p => ':froboz'
;; No matching field found: getP for class java.io.File
;; {:name nil, :absolute-path nil, :froboz nil}
;; Well, if I cannot get it to work as a function, can I make it work as a
;; macro
(defmacro mget**
[^Object o & properties]
`(reduce (fn [m# p#]
(println (format "m => '%s'; p => '%s'" m# p#))
(assoc m# p# (try-get* ~o p#)))
{} (quote ~properties)))
(mget** (File. "/etc/hostname") :name :absolute-path :froboz)
;; also bizarre; again, the *name* of the second variable to the
;; anonymous function is being captured instead of its *binding* --
;; and I've no clue why!
;; m => '{}'; p => ':name'
;; No matching field found: getP14571Auto for class java.io.File
;; m => '{:name nil}'; p => ':absolute-path'
;; No matching field found: getP14571Auto for class java.io.File
;; m => '{:name nil, :absolute-path nil}'; p => ':froboz'
;; No matching field found: getP14571Auto for class java.io.File
;; {:name nil, :absolute-path nil, :froboz nil}
;; So let's try a function which DOESN'T call the macro...
(defn fget**
[^Object o & properties]
(reduce (fn [m p]
(let [s (->camelCase (str "get-" (name p)))]
(println (format "m => '%s'; p => '%s'; s => '%s'" m p s))
(assoc m p (eval (list '. o (symbol s))))))
{} properties))
(fget** (File. "/etc/hostname") :absolute-path)
;; Curiouser and curiouser. Also fails:
;; m => '{}'; p => ':absolute-path'; s => 'getAbsolutePath'
;; Syntax error compiling fn* at (/tmp/form-init1099311198524902517.clj:1:1).
;; Can't embed object in code, maybe print-dup not defined: /etc/hostname
;;
;; I don't *think* it's the explicit anonymous function which fails to compile
;; because the println happens; therefore, `eval` is calling the compiler to
;; compile the expression passed to it, and it's failing there.
;; Right, can we ask what getter methods an object has? Well, that isn't easy.
;; but we can introspect what accessible properties a Java object has, using
;; `clojure.lang/bean`, which is almost the same thing. The `bean` function
;; returns camel-cased key names, which is not what I want, but hey...
(defn get**
"Get multiple `properties` from this single object `o`. Properties are
expected to be specified as kebab-case keywords, mainly for consistency
with `get*`"
[^Object o & properties]
(let [mung (bean o)]
(select-keys (reduce (fn [m p]
(assoc m (->kebab-case-keyword p) (mung p)))
{}
(keys mung))
(filter keyword? properties))))
(get** (File. "/etc/hostname") :path :absolute-path :directory)
;; Well, it works.
;; But this is yet another case of Lisp programmers knowing the value of
;; everything, but the cost of nothing. We're pulling *all* of the properties
;; of a Java object to read the values of only some of them, and, Java not
;; being a pure functional language, we have no means of knowing how expensive
;; or how side-effecty the getter methods involved are.
;; I'm also feeling really humbled (and puzzled) by the failure of my earlier
;; attempts!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment