Prototype code for enhanced CIDER code completion for Java methods

459 views
Skip to first unread message

somewhat-functional-programmer

unread,
Oct 11, 2018, 8:22:37 PM10/11/18
to clo...@googlegroups.com
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 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:


(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)}))

    (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
(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.


Didier

unread,
Oct 15, 2018, 12:59:57 PM10/15/18
to Clojure
Could you find a way that doesn't require a syntax change?

Maybe a meta hint?

Tatu Tarvainen

unread,
Oct 16, 2018, 11:47:02 AM10/16/18
to Clojure
Nice. I like the syntax, I'll try it out.

But it seems unlikely to me that the interop forms would be changed in core at this late stage.

As metadata tags can be added to symbols, could we write (^String .charAt "test-string" 0) 
It doesn't look as nice as your proposed syntax, but is possible without modifications.

Timothy Baldridge

unread,
Oct 16, 2018, 12:23:44 PM10/16/18
to clo...@googlegroups.com
I don't understand why this is needed. Why can't cider code complete on the normal method format? What's the problem this code is trying to solve?

Timothy

somewhat-functional-programmer

unread,
Oct 16, 2018, 3:26:04 PM10/16/18
to clo...@googlegroups.com
I apologize for diving into a solution in my first email -- let me give a little more of the background as to what I am trying to accomplish.

I'm proposing an additional syntax for Java interop for the purpose of allowing support for more precise code-completion of Java fields and methods in Clojure tooling.  The new syntax proposal is why I'm emailing this list rather than directly going to the project owners of the projects I've modified to support this.  If there is high resistance to any additional syntax for Java interop, there's not much reason to try to convince the project owners of cider-nrepl and compliment to support something just for my personal use :-).

Note also that when I say "propose additional syntax" I do not mean "please add this to clojure lang" -- I've implemented this as a couple of macros which should be the way the language normally gets extended.  I just want feedback and am trying to gauge interest, because if no one wants this then it's not worth any of the additional effort to "publish" it -- I'll just manage it as a utility library for myself.

So when I say more precise code completion for Java fields and methods, I mean this:
  - I want my development environment or REPL to know the type of the Java object I'm operating on so it can limit the completion list to fields or methods that are valid for that particular type.
    - For example, when typing:
      (.get
      I'd somehow like my environment to know that I want code completion for a Java object of type "this.is.AType"
      and as such, I would only see methods or fields starting with ".get" for that type
    What happens for me now, is I see some Java methods and fields starting with ".get" but on a number of Java objects (and not a complete list at that).
      (I believe the tooling looks through symbols in your namespace, finds Java symbols, and starts pulling methods and fields from any of them).  Using only the text "(.get" the environment cannot know what I'm trying to do.

    Now the tooling could do static code analysis, it could look at the surrounding top-level form, and say, what are the locals, are they type-hinted, and only show me Java methods and fields from locals that have type hints.  Even this approach though, which is complex and error prone to implement, still doesn't limit my list to just methods and fields of "this.is.AType" unless I only have one type-hinted local symbol.
   
So...  if we had a syntax that looked like:
(Type:method obj? args)
My tooling becomes drastically simpler.  Now my tooling is fed:
  "Type:method"
And code completion simply becomes:
  - Look up Type in my active namespace symbols (to find out if it's a Java type I've imported) (or it could simply be fully qualified, both are supported)
  - Use reflection to find a list of candidate fields and methods
No static analysis required, simple to implement.

It's so simple to implement that I hastily pasted in my proof-of-concept/prototype code in the first message -- which hopefully is easy for folks to just try.  I hope that people will try it and think: this would be great if I were writing a lot of Java interop code -- or better yet, give suggestions on how to improve it.

So try this by:
1) Install boot (or update it via "boot -u")
2) Make a new temp directory and navigate to it
3) Run the first command from my prior email in bash
4) Paste in the code snippet to the resulting rebel-readline repl
5) Try the limited completion right in rebel-readline
6) Connect using your cider-nrepl based tooling (CIDER, inf-clojure, vim-fireplace, others?), and try it from one of those tools.  I've only tested with CIDER -- in my CIDER I get eldoc, help at symbol at point (I wish this displayed javadoc inline), and better code completion.
7) Feedback! :-)

I also realize that this stuff is complex enough that there might be some set of CIDER settings that solve this problem that I just simply don't know about or didn't find.  Love to hear about how others get better Java interop code completion.  Some of my clojure projects involve lots of Java interop and this would have been sooooo nice to have while writing those projects (in my opinion).

I think there are other advantages to this style syntax (besides supporting code completion):
  - Right now I have the jvm macro throw an error if it can't find the java symbol -- so you would get a compile time error rather than just a reflection warning about a non-existent java method.
  - In addition to making easier to *write* Java interop, I think it makes it easier to *read* Java interop
    I immediately know the Java type while reading the code, no need to hunt down the type annotations elsewhere.  I've been writing Java since Java 1.1 and I still like the reminder of what class a method is from.


‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Tuesday, October 16, 2018 4:23 PM, Timothy Baldridge <tbald...@gmail.com> wrote:

I don't understand why this is needed. Why can't cider code complete on the normal method format? What's the problem this code is trying to solve?

Timothy


--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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
For more options, visit this group at
---
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+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

somewhat-functional-programmer

unread,
Oct 16, 2018, 3:45:24 PM10/16/18
to clo...@googlegroups.com
Comments inline...

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Tuesday, October 16, 2018 3:47 PM, 'Tatu Tarvainen' via Clojure <clo...@googlegroups.com> wrote:

Nice. I like the syntax, I'll try it out.

Thanks -- let me know if the boot command doesn't work...  I had to update boot on one of my boxes to get it to work ("boot -u" -- to 2.8.2 I think).

But it seems unlikely to me that the interop forms would be changed in core at this late stage.

I agree -- I probably should have worded "propose new syntax" differently -- this was never intended to be a candidate for clojure lang -- it was always intended to be a macro similar to the prototype code I put in my last message.

As metadata tags can be added to symbols, could we write (^String .charAt "test-string" 0) 
It doesn't look as nice as your proposed syntax, but is possible without modifications.

Interesting -- I don't believe I'd get the full "(^String .prefix" in the place in the compliment library where I made my addition (as the "prefix") -- but I would get the "context" (from some but potentially not all clojure tooling) -- the context being the surrounding top-level form which would include the "^String" -- so in theory it would be possible for some clojure tooling to use this to accomplish the same thing.

I think it would look odd for those places where you still had to type hint to avoid reflection:
(defn avoid-reflection-warning-by-doing-this [a]
  (^InputStream .read ^InputStream a))

(The macro on the other hand automatically adds the type hint to avoid reflection)

Timothy Baldridge

unread,
Oct 16, 2018, 3:46:48 PM10/16/18
to clo...@googlegroups.com
As you say, this is a limitation in the code completer. In Cursive this problem doesn't exist to this extent, when I type `(.get` the completer responds with a list of methods and classes that could be completed to that method, starting with classes in the namespace I'm currently editing.

Yes, this takes static analysis, or perhaps some indexing and introspection. But changing the syntax is never really going to gain traction here, as manually typing the name of every method just so my editor can produce a list seems like me conforming to my tool rather than my tool learning to work with me.

The imported classes in the current namespace are stored in the mappings attribute. It seems like an editor should be able to install hooks into that and index the methods on the classes to provide this feedback. https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Namespace.java#L123 
--
“One of the main causes of the fall of the Roman Empire was that–lacking zero–they had no way to indicate successful termination of their C programs.”
(Robert Firth)

somewhat-functional-programmer

unread,
Oct 16, 2018, 8:30:08 PM10/16/18
to clo...@googlegroups.com
Comments inline...  I really appreciate you taking the time to look at this.  I think I am still imprecise in my language -- I hope the comments below doesn't come across as too tedious :-)...

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Tuesday, October 16, 2018 7:46 PM, Timothy Baldridge <tbald...@gmail.com> wrote:

As you say, this is a limitation in the code completer. In Cursive this problem doesn't exist to this extent, when I type `(.get` the completer responds with a list of methods and classes that could be completed to that method, starting with classes in the namespace I'm currently editing.

I think we are saying the same thing here.  I believe compliment (the library CIDER/other clojure tooling uses for code completion) does what we are describing (showing Java methods and fields from multiple Java types that are imported into the namespace currently being edited (or type hinted in locals/function definitions).  My point is I want more -- I want the completion list to only include methods and fields from the type I as a developer know that I have.

Like a Java IDE:
MyType a = new MyType();
Now typing "a." yields just completions valid for MyType, and not 5 other types I've used nearby.

Yes, this takes static analysis, or perhaps some indexing and introspection. But changing the syntax is never really going to gain traction here, as manually typing the name of every method just so my editor can produce a list seems like me conforming to my tool rather than my tool learning to work with me.

Just to make sure I'm completely clear -- I'm *not* advocating a change to clojure lang -- only proposing a macro/library.  The prototype macro I wrote simply transforms the (Type:method ...) syntax into the (. instance-expr member-symbol) that (.method ...) macroexpands into anyhow.  This is not intended to replace (.method obj args) notation.

I'm not sure what you mean by "manually typing the name of every method just so my editor can produce a list".

The way this works is, you type "(MyType:" and get presented with a list of completions that are *only* applicable to MyType -- there's no manually typing lists of methods and fields anywhere.  And, the way this works for me in CIDER, I type "(My<TAB>" and I get "(MyType", then I add a ":", and now I get completions just for MyType -- it also shows me the Java signature of the methods as I highlight a potential completion.  There's no manually seeding the list anywhere...

The imported classes in the current namespace are stored in the mappings attribute. It seems like an editor should be able to install hooks into that and index the methods on the classes to provide this feedback. https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Namespace.java#L123 

Yes I agree -- and I believe that's exactly what the existing tooling already does -- I just want more precision.

Some of my motivation has been working with monstrously large class libraries in Java.  GIS libraries in Java are notoriously class/method heavy and after importing many of these classes into a namespace, completion that cannot narrow down to the exact type I'm using for the reasons we agree on -- simply is painful to work with.

Aaron Cohen

unread,
Oct 16, 2018, 11:56:37 PM10/16/18
to clo...@googlegroups.com
This seems like it could be done using threading.

(-> "my-string"
   (.ch    <-- completion should give you good results here, for only String methods


(-> (new MyType)
      (.    <-- completion should give you only methods of MyType


; One interesting case is the following:
(def foo "hello")

; foo's type isn't known, so would need to be hinted
(-> ^String foo
    (.to  <-- good completions again, because of the type hint

I think you won't be able to get all the way to your jvm macro, but likely pretty close, and it's much more idiomatic...

The doto macro is also useful in a similar way, and often what you want when using some of the more byzantine java libraries.

(All of the above works in Cursive, I'm not sure about how it works in CIDER, but I assume it's equivalent).

--Aaron


Didier

unread,
Oct 17, 2018, 12:34:07 AM10/17/18
to Clojure
How does the new syntax help the tooling figure out the type?

(def var (SomeType.))
(.method var)

Or

(jvm (var.method))

I'm not sure how you narrow down to only the SomeType methods?

Colin Fleming

unread,
Oct 17, 2018, 4:41:14 AM10/17/18
to clo...@googlegroups.com
Cursive already allows this with no syntax changes, but it does require reproducing Clojure's type inference in the editor. Cursive performs this type inference, so (modulo bugs) it knows the types of basically everything that the Clojure compiler does (and in fact in some situations can do better, but I deliberately use just what the compiler knows right now).

This is used in various ways in completion. All of the cases that Aaron enumerates above work, since Cursive can correctly propagate the types in threading forms/doto etc (| is the caret):

(-> "my-string"
    (.ch|))    ; <- Only String methods here

(-> (new MyType)
    (.|))      ; <- MyType methods here

(def foo "hello")

(-> ^String foo
    (.to|))    ; <- String completions here too, because of the type hint.
               ;    Cursive can do better here even though the Clojure compiler doesn't.
               ;    Currently I restrict this, but completing could e.g. automagically insert the type hint.

(let [foo "string"]
  (-> foo (.to|)) ; <- No type hint required, since we know the type of foo

Additionally, Cursive supports the following:

(let [foo (ArrayList.)
      iterator (.iterator foo)]
  (.ne|))      ; <- Here, .next will be offered even though Iterator isn't imported,
               ;    because Cursive knows the types we have in scope at the caret.

(let [foo (ArrayList.)]
  (foo .i|))   ; <- Here, I've put the receiver first, then I'm completing the method call.
               ;    Since Cursive knows the type of foo, only ArrayList methods will be completed.
               ;    When I select a completion, Cursive will swap the two around, like:
(let [foo (ArrayList.)]
  (.iterator foo|))

I use this last one all the time, and it basically makes Clojure completion as good as Java completion. Someone pointed out to me that this should really use the existing dot syntax, like:

(let [foo (ArrayList.)]
  (. foo it|))  ; <- completes to:

(let [foo (ArrayList.)]
  (.iterator foo|))

But I haven't implemented that yet.

I don't do any of the more tricky stuff that IntelliJ does like transitively searching across chained method calls based on the type, etc, but that's just a time issue - there's nothing preventing Cursive from doing that too. Also, offering completions from classes that are not imported but are in scope makes a huge difference, even without any switching trickiness.

Cheers,
Colin

somewhat-functional-programmer

unread,
Oct 17, 2018, 7:13:11 AM10/17/18
to clo...@googlegroups.com
I appreciate your detailed response, and you've certainly done great work with Cursive.  I always recommend it to any Java programmer who is starting to learn Clojure.  I will start to more seriously weigh the pros and consadditional of switching away from emacs.  The cult of emacs has had a strong pull on me but your good work may mean I should leave it for Clojure work too (and not just for Java work :-)).

Cursive is doing much more in trying to match the Clojure compiler than I believe the compliment library has done to date.  Part of me likes the readability of the syntax:
(String:charAt my-string 0)
over
(-> ^String my-string (.charAt 0))
But I realize that is much more subjective than anything else.  I have to say, in delving into this even a little -- I appreciate the time you must have spent matching the Clojure compiler.  I just remember looking at Kawa's syntax for Java method calls and thinking, wow, I wish Clojure had that -- so much more readable!

I like your last example a lot:
(foo .method) => getting turned into (.method foo) automatically by the editor.
I've actually looked at doing a similar thing with my macro -- basically using it in my editor, and then adding an nREPL middleware to transform it to the much beloved (.method obj args) notation.  Since I subjectively like the readability of (String:charAt obj 0) better than (.charAt ^String obj 0) I didn't go that route in this discussion.

I'm curious though, why additional macro / slightly differences from "idiomatic" seems so important to avoid.  I think something as simple as (String:charAt obj 0) notation would be pretty simple for a third syntax (since we already have two built in ways of doing it) -- and closer to the static method invocation syntax -- so really a third built-in syntax (Integer/parseInt "12").  But that's more a philosophical question I suppose :-).  LISP curse anyone? :-)

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐

somewhat-functional-programmer

unread,
Oct 17, 2018, 7:16:50 AM10/17/18
to clo...@googlegroups.com
It's not really:
(jvm (var.method))
but
(jvm (JavaType:method var))

Because the completion engines get "JavaType:" as a prefix, they can look up the type via reflection and present a list of methods for *just* that type.

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@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+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
>
> ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

somewhat-functional-programmer

unread,
Oct 17, 2018, 7:20:02 AM10/17/18
to clo...@googlegroups.com
I appreciate your thoughtful response -- I wish some of the other tooling could do this level of analysis but I can only imagine the time it took Colin to implement :-).  Like I mentioned in my response to him -- I'm going to have to seriously consider leaving the cult of emacs not only for Java but maybe Clojure too :-).

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐

Alan Thompson

unread,
Oct 17, 2018, 11:17:00 AM10/17/18
to clojure
I love Cursive and use the IntelliJ Vim keybindings everyday.  Apparently they have alternate keybindings for those of you from the dark side.
Alan

Matching Socks

unread,
Oct 19, 2018, 5:58:38 AM10/19/18
to Clojure
The cost/benefit is hard to work out.  Look ahead 7 years - we would have the ceremonial (jvm... (MyType:baz...)) in some places and the good old elegant (.baz ...) in others, and the tooling will work just as well with it.  On the other hand, if you routinely do tons of interop with a wide variety of classes whose method names are subtly distinct and mostly 40 letters or more, then (jvm...) will seem like genius!  Nonetheless, I hope your work will inspire code completers to go the extra mile, once and for all.  In the meantime, you could wrap that dreadful interop with a library whose method names are shorter.

Colin Fleming

unread,
Oct 20, 2018, 6:14:40 AM10/20/18
to clo...@googlegroups.com
I'm curious though, why additional macro / slightly differences from "idiomatic" seems so important to avoid.

I can think of a couple of reasons off the top of my head:
  1. It won't work with any tooling that's unaware of it, e.g. Cursive.
  2. It's very verbose - it's even worse than Java, because instead of annotating each variable once where it's declared you have to annotate every method call, and those annotations remain forever in the code, not just when you're writing it. This is true even when the type is trivially obvious, e.g. (String:charAt "my-string" 0).Modern languages are moving towards more sophisticated type inference for a reason - it's much more comfortable.
Really, this feature is just to make editors work better. Why not do something similar to what Cursive does, and allow the user to type (String:ch|) . Then just show the method completions from String and when the user selects "charAt" just convert to (.charAt |)? This is really a feature to aid code writing after all, not reading. Then the feature wouldn't require macros at all, it could be a purely Emacs thing.

somewhat-functional-programmer

unread,
Oct 21, 2018, 3:17:01 PM10/21/18
to clo...@googlegroups.com
Comments inline...

I appreciate the discussion to date!

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Saturday, October 20, 2018 10:13 AM, Colin Fleming <colin.ma...@gmail.com> wrote:

I'm curious though, why additional macro / slightly differences from "idiomatic" seems so important to avoid.

I can think of a couple of reasons off the top of my head:
  1. It won't work with any tooling that's unaware of it, e.g. Cursive.
I agree -- my goal here isn't to add work everywhere.  I think most Cursive users probably would care less if it isn't added to Cursive, and if there was an actual demand, at least the code to implement would already be done in another library, and a trivial amount of code.

One nice thing about cider-nrepl/compliment is that multiple tools use them -- I like having this code complete from rebel-readline for example, and in theory (I say this because I haven't tested it), it should work in vim-fireplace/vim-replant/etc.

I like using it from a rebel-readline REPL over SSH for example -- completely outside of emacs/CIDER.

I think this "style" of tooling (not my syntax -- but tooling that is always integrated/present in the clojure process) yields this type of benefit -- I can use it from wherever I run my code if I'd like, actually independent of any editor or development environment.
  1. It's very verbose - it's even worse than Java, because instead of annotating each variable once where it's declared you have to annotate every method call, and those annotations remain forever in the code, not just when you're writing it. This is true even when the type is trivially obvious, e.g. (String:charAt "my-string" 0).Modern languages are moving towards more sophisticated type inference for a reason - it's much more comfortable.
Really, this feature is just to make editors work better. Why not do something similar to what Cursive does, and allow the user to type (String:ch|) . Then just show the method completions from String and when the user selects "charAt" just convert to (.charAt |)? This is really a feature to aid code writing after all, not reading. Then the feature wouldn't require macros at all, it could be a purely Emacs thing.

I agree -- if I knew more about emacs lisp I would have gone down this route initially.  However, my personal opinion is that this syntax does improve readability, at the price of additional verbosity -- a verbosity I personally don't mind while editing/writing because my editor helps me out.  The syntax itself is similar to C++/Java lambda method references...  Type:method.  I love immediately seeing what class a method is from when looking over code where I'm unfamiliar with the Java types being used.  But I agree -- this is subjective, and if I had more emacs lisp knowledge/time I would have thought much more about that route.  My main goal posting here was:
- Hey try this syntax out, it's helped me tremendously with Java interop in CIDER, and hopefully elicit some feedback (and I appreciate yours)
I hope it's obvious from the idea of pasting code into a repl to try the feature that this isn't *done/released/supported/warrantied/whatever*, but really is the beginnings of an idea being explored.  In fact, since I'm now starting to use this code more in one of my new interop-heavy projects, I've already made a couple of minor changes to it.  It took me much longer to find a minimal boot command that used tools/deps/cli and a paste-able code-snippet than writing it in the first place :-).

I don't see how this is related to type-inferencing at all.  Type-hinting the way we do in clojure is really a type declaration -- it isn't really trying to infer that say, something is a number based on usage (say, by being a participant in the '+' function --- on the contrary, if I want smart numerical "typing", I have to ^long or ^double).  Clojure simply casts/boxes to whatever the interop function expects.  If you're referring to the new Java syntax of 'var x = new blah();' -- I think that's mainly a reaction to How<Many<<Templates<Can<we<Have<and>Still<Be<readAble>>>>>...
I'd almost argue what we do in Clojure is more like gradual typing -- I start to type for performance/interop reasons (numerical computing or Java interop) but only sparingly.  I don't doubt you are doing type inference in Cursive -- sounds like that's precisely how you are getting good completion lists.

somewhat-functional-programmer

unread,
Oct 21, 2018, 3:24:13 PM10/21/18
to clo...@googlegroups.com
I agree :-).  For me the cost/benefit decision is easy -- I'm using it in a new interop-heavy project.  I posted it here in case others who used cider-nrepl based tooling may also potentially benefit -- if enough interest maybe I'd take the extra time to publish it as a library.

Often times I have had small macros / functions that I imagine would be of use to others but never know how to share them.  I personally have a bad reaction against pulling in a named dependency for 1 or 2 functions -- how to share such things?  I bet most developers in a lisp start to have their own utility libraries.  I've always imagined there would be much for me to learn by seeing those libraries from seasonsed lispers.

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Friday, October 19, 2018 9:58 AM, Matching Socks <phill...@gmail.com> wrote:

The cost/benefit is hard to work out.  Look ahead 7 years - we would have the ceremonial (jvm... (MyType:baz...)) in some places and the good old elegant (.baz ...) in others, and the tooling will work just as well with it.  On the other hand, if you routinely do tons of interop with a wide variety of classes whose method names are subtly distinct and mostly 40 letters or more, then (jvm...) will seem like genius!  Nonetheless, I hope your work will inspire code completers to go the extra mile, once and for all.  In the meantime, you could wrap that dreadful interop with a library whose method names are shorter.


Alexander Yakushev

unread,
Nov 25, 2018, 12:53:06 PM11/25/18
to Clojure
To anyone who's interested in having precise completion for Java classes – a feature like this is available in the latest CIDER snapshot: https://twitter.com/unlog1c/status/1066748173094453248. You can get a filtered completion by prepending a type tag to the next symbol.

somewhat-functional-programmer

unread,
Nov 26, 2018, 8:40:59 PM11/26/18
to clo...@googlegroups.com
Very cool!  By the way, thanks for all the work you have put in to compliment.  Sometimes it's hard to know when/how to thank people.  I myself am all too often guilty of, "Thanks, can I have a new feature or bug fix?"

You, Mr. Emerick, Mr. Batsov -- and many others -- thanks!

I'd start a thanksgiving thread for clojure, but I'm afraid my list would be quite long, and sort of sound presumptuous, as if I'd accomplished something worthy of thanking the many folks whose work made it possible.  So how about this --
  Thanks to all the different folks in the community for sharing your work, whether I've used it or not, I've learned many things from:
  - Presentations by Rich Hickey
  - The Joy of Clojure
  - CIDER
  - Figwheel and ClojureScript
  - core.async, manifold, ring, sente, timbre, specter, seesaw, ...

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
Reply all
Reply to author
Forward
0 new messages