'somewhat-functional-programmer' via Clojure
2018-10-12 00:22:23 UTC
I'd like to share an idea and prototype code for better Java code completion in CIDER. While my main development environment is CIDER, the small modifications I made to support this idea were both to cider-nrepl and compliment -- which are both used by other Clojure tooling besides CIDER -- so maybe this is immediately more widely applicable.
In an effort to make it easier on the tooling, I'm using a slightly different syntax for calling Java methods. My inspiration is Kawa scheme, and the notation is very similar:
(String:charAt "my-string" 0) => \m
(Integer:parseInt "12") => 12
(possibly.fully.qualified.YourClassName:method this args)
For this syntax to be properly compiled of course it needs to be wrapped in a macro:
One form:
(jvm (String:charAt "my-string" 0))
Any number of forms:
(jvm
(lots of code)
(JavaClass:method ...)
(more code)
(AnotherJavaClass:method ...))
The jvm macro will transform any symbol it finds in the calling position of a list that follows the ClassName:method convention. I was thinking maybe of limiting it to just a particular namespace to absolutely prevent any name collisions with real clojure functions, something like:
(jvm/String:charAt "my-string" 0)
This will also work with the one-off test code I'm including here for folks to see what they think.
I actually like the syntax (though I wish I didn't have to wrap it in a jvm macro -- though if this actually idea was worth fully implementing, I'd imagine having new let or function macros so you don't even have to sprinkle "jvm" macros in code much at all).
There is one additional advantages to this style of Java interop besides the far better code completion:
- The jvm macro uses reflection to find the appropriate method at compile time, and as such, you get a compile error if the method cannot be found.
- This is a downside if you *want* reflection, but this of course doesn't preclude using the normal (.method obj args) notation.
You could even use this style for syntactic sugar for Java method handles:
- Though not implemented in my toy code here, you could also pass String:charAt as a clojure function -- assuming there were no overloads of the same arity.
So, I'm hoping you will try this out. Two things to copy/paste -- one is a boot command, the other is the 100-200 lines of clojure that implements a prototype of this.
This command pulls the necessary dependencies as well as starts up the rebel-readline repl (which is fantastic tool, and it also uses compliment for code completion):
# Run this somewhere where you can make an empty source directory,
# something fails in boot-tools-deps if you don't have one
# (much appreciate boot-tools-deps -- as cider-nrepl really needs to
# be a git dep for my purpose here since it's run through mranderson for its normal distro)
mkdir src && \
boot -d seancorfield/boot-tools-deps:0.4.6 \
-d compliment:0.3.6 -d cider/orchard:0.3.1 \
-d com.rpl/specter:1.1.1 -d com.taoensso/timbre:4.10.0 \
-d com.bhauman/rebel-readline:0.1.4 \
-d nrepl/nrepl:0.4.5 \
deps --config-data \
'{:deps {cider/cider-nrepl {:git/url "https://github.com/clojure-emacs/cider-nrepl.git" :sha "b2c0b920d762fdac2f8210805df2055af63f2eb1"}}}' \
call -f rebel-readline.main/-main
Paste the following code into the repl:
(require 'cider.nrepl.middleware.info)
(ns java-interop.core
(:require
[taoensso.timbre :as timbre
:refer [log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy get-env]]
[clojure.reflect :as reflect]
[clojure.string :as s :refer [includes?]]
[com.rpl.specter :as sp]
[orchard.java :as java]))
(defn specific-class-member? [prefix]
;; NOTE: get a proper java class identifier here
(when-let [prefix (if (symbol? prefix)
(name prefix)
prefix)]
(and
(not (.startsWith prefix ":"))
(not (includes? prefix "::"))
(includes? prefix ":"))))
(def select-j-path
(sp/recursive-path
[] p
(sp/cond-path
#(and (seq? %) (specific-class-member? (first %)))
[(sp/continue-then-stay sp/ALL-WITH-META p)]
map? (sp/multi-path
[sp/MAP-KEYS p]
[sp/MAP-VALS p])
vector? [sp/ALL-WITH-META p]
seq? [sp/ALL-WITH-META p])))
(defmacro j [[s & [obj & args]]]
;; FIXME: Add better error checking later
;; FIXME: Java fields can have the same name as a method
(let [[clazz-str method-or-field & too-many-semis] (.split (name s) ":")
method-or-field-sym (symbol method-or-field)
clazz-sym (symbol clazz-str)]
(if-let [{:keys [flags return-type]} (first
(filter
#(= (:name %) method-or-field-sym)
(:members
(reflect/reflect
(ns-resolve *ns* clazz-sym)
:ancestors true))))]
(cond
(contains? flags :static) (concat
`(. ~clazz-sym ~method-or-field-sym)
(if obj
`(~obj))
args)
:else
(concat
`(. ~(if (symbol? obj)
(with-meta
obj
{:tag clazz-sym})
obj)
~(symbol method-or-field))
args))
(throw (ex-info "Method or field does not exist in class."
{:method method-or-field-sym
:class clazz-sym})))))
(defmacro jvm [& body]
(concat
`(do)
(map
#(sp/transform
select-j-path
(fn [form]
`(j ~form))
%)
body)))
;; for compliment code complete
(in-ns 'compliment.sources.class-members)
(require 'java-interop.core)
(defn members-candidates
"Returns a list of Java non-static fields and methods candidates."
[prefix ns context]
(cond
(class-member-symbol? prefix)
(let [prefix (subs prefix 1)
inparts? (re-find #"[A-Z]" prefix)
klass (try-get-object-class ns context)]
(for [[member-name members] (get-all-members ns)
:when (if inparts?
(camel-case-matches? prefix member-name)
(.startsWith ^String member-name prefix))
:when
(or (not klass)
(some #(= klass (.getDeclaringClass ^Member %)) members))]
{:candidate (str "." member-name)
:type (if (instance? Method (first members))
:method :field)}))
(java-interop.core/specific-class-member? prefix)
(let [sym (symbol prefix)
[clazz-str member-str & too-many-semis] (.split (name sym) #_prefix ":")]
(when (not too-many-semis)
(when-let [clazz
(resolve-class ns (symbol clazz-str))]
(->>
(clojure.reflect/reflect clazz :ancestors true)
(:members)
(filter #(and
;; public
(contains? (:flags %) :public)
;; but not a constructor
(not (and (not (:return-type %)) (:parameter-types %)))
;; and of course, the name must match
(or
(clojure.string/blank? member-str)
(.startsWith (str (:name %)) member-str))))
(map
(fn [{:keys [name type return-type]}]
{:candidate (str (when-let [n (namespace sym)]
(str (namespace sym) "/")) clazz-str ":" name)
:type (if return-type
:method
:field)}))))))))
;; for eldoc support in cider
(in-ns 'cider.nrepl.middleware.info)
(require 'orchard.info)
(defn java-special-sym [ns sym]
(let [sym-str (name sym)]
(if (clojure.string/includes? sym-str ":")
(when-let [[class member & too-many-semis] (.split sym-str ":")]
(if (and class
member
(not too-many-semis))
(when-let [resolved-clazz-sym
(some->>
(symbol class)
^Class (compliment.utils/resolve-class ns)
(.getName)
(symbol))]
[resolved-clazz-sym
(symbol member)]))))))
(defn info [{:keys [ns symbol class member] :as msg}]
(let [[ns symbol class member] (map orchard.misc/as-sym [ns symbol class member])]
(if-let [cljs-env (cider.nrepl.middleware.util.cljs/grab-cljs-env msg)]
(info-cljs cljs-env symbol ns)
(let [var-info (cond (and ns symbol) (or
(orchard.info/info ns symbol)
(when-let [[clazz member]
(java-special-sym ns symbol)]
(orchard.info/info-java clazz member)))
(and class member) (orchard.info/info-java class member)
:else (throw (Exception.
"Either \"symbol\", or (\"class\", \"member\") must be supplied")))
;; we have to use the resolved (real) namespace and name here
see-also (orchard.info/see-also (:ns var-info) (:name var-info))]
(if (seq see-also)
(merge {:see-also see-also} var-info)
var-info)))))
;; cider blows up if we don't have a project.clj file for it to read the version
;; string from
(ns cider.nrepl.version
;; We require print-method here because `cider.nrepl.version`
;; namespace is used by every connection.
(:require [cider.nrepl.print-method]
[clojure.java.io :as io]))
#_(def version-string
"The current version for cider-nrepl as a string."
(-> (or (io/resource "cider/cider-nrepl/project.clj")
"project.clj")
slurp
read-string
(nth 2)))
(def version-string "0.19.0-SNAPSHOT")
(def version
"Current version of CIDER nREPL as a map.
Map of :major, :minor, :incremental, :qualifier,
and :version-string."
(assoc (->> version-string
(re-find #"(\d+)\.(\d+)\.(\d+)-?(.*)")
rest
(map #(try (Integer/parseInt %) (catch Exception e nil)))
(zipmap [:major :minor :incremental :qualifier]))
:version-string version-string))
(defn cider-version-reply
"Returns CIDER-nREPL's version as a map which contains `:major`,
`:minor`, `:incremental`, and `:qualifier` keys, just as
`*clojure-version*` does."
[msg]
{:cider-version version})
(in-ns 'boot.user)
(require 'nrepl.server)
(defn nrepl-handler []
(require 'cider.nrepl)
(ns-resolve 'cider.nrepl 'cider-nrepl-handler))
(nrepl.server/start-server :port 7888 :handler (nrepl-handler))
(require '[java-interop.core :refer [jvm]])
;; NOTE: Code completion works in rebel-readline,
;; but it limits how many completions are shown at once
;; Try CIDER (you have an nrepl instance running now localhost:7888)
;; Eldoc also works in cider
;;
;; example
(jvm
[(Integer:parseInt "12") (String:charAt "test-string" 0)])
You should now have an nrepl server running on localhost:7888 which you can connect to from CIDER. You can try the completion (though not documentation) right in rebel-readline's repl.
So give it a try.... you'll notice static fields aren't handled perfectly (easy fix, but again I really am looking for feedback on the concept, and am wondering who would use it etc).
Right now you can access a static field like a method call:
(jvm (Integer:MAX_VALUE))
I think the code completion + eldoc in CIDER is a productivity boost for sure.
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to ***@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+***@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
In an effort to make it easier on the tooling, I'm using a slightly different syntax for calling Java methods. My inspiration is Kawa scheme, and the notation is very similar:
(String:charAt "my-string" 0) => \m
(Integer:parseInt "12") => 12
(possibly.fully.qualified.YourClassName:method this args)
For this syntax to be properly compiled of course it needs to be wrapped in a macro:
One form:
(jvm (String:charAt "my-string" 0))
Any number of forms:
(jvm
(lots of code)
(JavaClass:method ...)
(more code)
(AnotherJavaClass:method ...))
The jvm macro will transform any symbol it finds in the calling position of a list that follows the ClassName:method convention. I was thinking maybe of limiting it to just a particular namespace to absolutely prevent any name collisions with real clojure functions, something like:
(jvm/String:charAt "my-string" 0)
This will also work with the one-off test code I'm including here for folks to see what they think.
I actually like the syntax (though I wish I didn't have to wrap it in a jvm macro -- though if this actually idea was worth fully implementing, I'd imagine having new let or function macros so you don't even have to sprinkle "jvm" macros in code much at all).
There is one additional advantages to this style of Java interop besides the far better code completion:
- The jvm macro uses reflection to find the appropriate method at compile time, and as such, you get a compile error if the method cannot be found.
- This is a downside if you *want* reflection, but this of course doesn't preclude using the normal (.method obj args) notation.
You could even use this style for syntactic sugar for Java method handles:
- Though not implemented in my toy code here, you could also pass String:charAt as a clojure function -- assuming there were no overloads of the same arity.
So, I'm hoping you will try this out. Two things to copy/paste -- one is a boot command, the other is the 100-200 lines of clojure that implements a prototype of this.
This command pulls the necessary dependencies as well as starts up the rebel-readline repl (which is fantastic tool, and it also uses compliment for code completion):
# Run this somewhere where you can make an empty source directory,
# something fails in boot-tools-deps if you don't have one
# (much appreciate boot-tools-deps -- as cider-nrepl really needs to
# be a git dep for my purpose here since it's run through mranderson for its normal distro)
mkdir src && \
boot -d seancorfield/boot-tools-deps:0.4.6 \
-d compliment:0.3.6 -d cider/orchard:0.3.1 \
-d com.rpl/specter:1.1.1 -d com.taoensso/timbre:4.10.0 \
-d com.bhauman/rebel-readline:0.1.4 \
-d nrepl/nrepl:0.4.5 \
deps --config-data \
'{:deps {cider/cider-nrepl {:git/url "https://github.com/clojure-emacs/cider-nrepl.git" :sha "b2c0b920d762fdac2f8210805df2055af63f2eb1"}}}' \
call -f rebel-readline.main/-main
Paste the following code into the repl:
(require 'cider.nrepl.middleware.info)
(ns java-interop.core
(:require
[taoensso.timbre :as timbre
:refer [log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy get-env]]
[clojure.reflect :as reflect]
[clojure.string :as s :refer [includes?]]
[com.rpl.specter :as sp]
[orchard.java :as java]))
(defn specific-class-member? [prefix]
;; NOTE: get a proper java class identifier here
(when-let [prefix (if (symbol? prefix)
(name prefix)
prefix)]
(and
(not (.startsWith prefix ":"))
(not (includes? prefix "::"))
(includes? prefix ":"))))
(def select-j-path
(sp/recursive-path
[] p
(sp/cond-path
#(and (seq? %) (specific-class-member? (first %)))
[(sp/continue-then-stay sp/ALL-WITH-META p)]
map? (sp/multi-path
[sp/MAP-KEYS p]
[sp/MAP-VALS p])
vector? [sp/ALL-WITH-META p]
seq? [sp/ALL-WITH-META p])))
(defmacro j [[s & [obj & args]]]
;; FIXME: Add better error checking later
;; FIXME: Java fields can have the same name as a method
(let [[clazz-str method-or-field & too-many-semis] (.split (name s) ":")
method-or-field-sym (symbol method-or-field)
clazz-sym (symbol clazz-str)]
(if-let [{:keys [flags return-type]} (first
(filter
#(= (:name %) method-or-field-sym)
(:members
(reflect/reflect
(ns-resolve *ns* clazz-sym)
:ancestors true))))]
(cond
(contains? flags :static) (concat
`(. ~clazz-sym ~method-or-field-sym)
(if obj
`(~obj))
args)
:else
(concat
`(. ~(if (symbol? obj)
(with-meta
obj
{:tag clazz-sym})
obj)
~(symbol method-or-field))
args))
(throw (ex-info "Method or field does not exist in class."
{:method method-or-field-sym
:class clazz-sym})))))
(defmacro jvm [& body]
(concat
`(do)
(map
#(sp/transform
select-j-path
(fn [form]
`(j ~form))
%)
body)))
;; for compliment code complete
(in-ns 'compliment.sources.class-members)
(require 'java-interop.core)
(defn members-candidates
"Returns a list of Java non-static fields and methods candidates."
[prefix ns context]
(cond
(class-member-symbol? prefix)
(let [prefix (subs prefix 1)
inparts? (re-find #"[A-Z]" prefix)
klass (try-get-object-class ns context)]
(for [[member-name members] (get-all-members ns)
:when (if inparts?
(camel-case-matches? prefix member-name)
(.startsWith ^String member-name prefix))
:when
(or (not klass)
(some #(= klass (.getDeclaringClass ^Member %)) members))]
{:candidate (str "." member-name)
:type (if (instance? Method (first members))
:method :field)}))
(java-interop.core/specific-class-member? prefix)
(let [sym (symbol prefix)
[clazz-str member-str & too-many-semis] (.split (name sym) #_prefix ":")]
(when (not too-many-semis)
(when-let [clazz
(resolve-class ns (symbol clazz-str))]
(->>
(clojure.reflect/reflect clazz :ancestors true)
(:members)
(filter #(and
;; public
(contains? (:flags %) :public)
;; but not a constructor
(not (and (not (:return-type %)) (:parameter-types %)))
;; and of course, the name must match
(or
(clojure.string/blank? member-str)
(.startsWith (str (:name %)) member-str))))
(map
(fn [{:keys [name type return-type]}]
{:candidate (str (when-let [n (namespace sym)]
(str (namespace sym) "/")) clazz-str ":" name)
:type (if return-type
:method
:field)}))))))))
;; for eldoc support in cider
(in-ns 'cider.nrepl.middleware.info)
(require 'orchard.info)
(defn java-special-sym [ns sym]
(let [sym-str (name sym)]
(if (clojure.string/includes? sym-str ":")
(when-let [[class member & too-many-semis] (.split sym-str ":")]
(if (and class
member
(not too-many-semis))
(when-let [resolved-clazz-sym
(some->>
(symbol class)
^Class (compliment.utils/resolve-class ns)
(.getName)
(symbol))]
[resolved-clazz-sym
(symbol member)]))))))
(defn info [{:keys [ns symbol class member] :as msg}]
(let [[ns symbol class member] (map orchard.misc/as-sym [ns symbol class member])]
(if-let [cljs-env (cider.nrepl.middleware.util.cljs/grab-cljs-env msg)]
(info-cljs cljs-env symbol ns)
(let [var-info (cond (and ns symbol) (or
(orchard.info/info ns symbol)
(when-let [[clazz member]
(java-special-sym ns symbol)]
(orchard.info/info-java clazz member)))
(and class member) (orchard.info/info-java class member)
:else (throw (Exception.
"Either \"symbol\", or (\"class\", \"member\") must be supplied")))
;; we have to use the resolved (real) namespace and name here
see-also (orchard.info/see-also (:ns var-info) (:name var-info))]
(if (seq see-also)
(merge {:see-also see-also} var-info)
var-info)))))
;; cider blows up if we don't have a project.clj file for it to read the version
;; string from
(ns cider.nrepl.version
;; We require print-method here because `cider.nrepl.version`
;; namespace is used by every connection.
(:require [cider.nrepl.print-method]
[clojure.java.io :as io]))
#_(def version-string
"The current version for cider-nrepl as a string."
(-> (or (io/resource "cider/cider-nrepl/project.clj")
"project.clj")
slurp
read-string
(nth 2)))
(def version-string "0.19.0-SNAPSHOT")
(def version
"Current version of CIDER nREPL as a map.
Map of :major, :minor, :incremental, :qualifier,
and :version-string."
(assoc (->> version-string
(re-find #"(\d+)\.(\d+)\.(\d+)-?(.*)")
rest
(map #(try (Integer/parseInt %) (catch Exception e nil)))
(zipmap [:major :minor :incremental :qualifier]))
:version-string version-string))
(defn cider-version-reply
"Returns CIDER-nREPL's version as a map which contains `:major`,
`:minor`, `:incremental`, and `:qualifier` keys, just as
`*clojure-version*` does."
[msg]
{:cider-version version})
(in-ns 'boot.user)
(require 'nrepl.server)
(defn nrepl-handler []
(require 'cider.nrepl)
(ns-resolve 'cider.nrepl 'cider-nrepl-handler))
(nrepl.server/start-server :port 7888 :handler (nrepl-handler))
(require '[java-interop.core :refer [jvm]])
;; NOTE: Code completion works in rebel-readline,
;; but it limits how many completions are shown at once
;; Try CIDER (you have an nrepl instance running now localhost:7888)
;; Eldoc also works in cider
;;
;; example
(jvm
[(Integer:parseInt "12") (String:charAt "test-string" 0)])
You should now have an nrepl server running on localhost:7888 which you can connect to from CIDER. You can try the completion (though not documentation) right in rebel-readline's repl.
So give it a try.... you'll notice static fields aren't handled perfectly (easy fix, but again I really am looking for feedback on the concept, and am wondering who would use it etc).
Right now you can access a static field like a method call:
(jvm (Integer:MAX_VALUE))
I think the code completion + eldoc in CIDER is a productivity boost for sure.
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to ***@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+***@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.