Created
May 27, 2024 12:54
-
-
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.
This file contains 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 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