I’ve been having some fun with Humble OOP system, trying to make fields declared in parent nodes visible to IDEs/editors
I don’t see easy way to make editors just “know” what fields come from parents, so they have to be somehow declared/referenced on a call-site. one idea I had was to switch from :extends Parent field to (extends Parent [par-field1 par-field2] (method-def [])) call, that would allow referencing only fields from given parent. then editor might treat this call as deftype , see field references and you could even add check, on (extends) macro level, to see whether fields you reference are actually declared on parent level
this one seemed a bit tedious though, so I went an implemented other idea, where parents declare not only their fields, but also declare “accessors” to those fields, that might be used in children extending those parents. the downside of this solution is that consumer who’s extending the parent has to access parents’ fields via those accessors, so it’s a bit more work. upside is that editors understand it easily, implementation was also pretty easy, and it makes code (maybe) bit more readable, because you directly see that field you access was declared on parent level
defparent and deftype+ that handle those changes live here https://github.com/fricze/hnhnhn/blob/main/src/oop.clj
defparent declares not only the definition map, as before, but also declares a record with those “field accessors”, so editors understand everything. you access field from the record, normal stuff
deftype+ has only one change, additional replace-parent-fields call on body. it looks for all those “field accessors” and replaces them with symbols for particular fields
https://github.com/fricze/hnhnhn/blob/main/src/scrollable.clj here you can see how it’s called
instead of writing (set! child-size (protocols/-measure child ctx child-cs)) I do (set! child-size (protocols/-measure (.child AWrapperNode) ctx child-cs))
so you see that child field is accessed via parent
another upside of this implementation is that when you try to access non-existent field on a parent it immediately throws, on a compile-time
I would risk an opinion that it looks fairly clean
@tonsky it’d need some polishing but curious whether you see something like that an acceptable solution for editor support of Humble parents fields
I was looking at consuming Java abstract classes from Clojure. it doesn’t look fun. there’s no way to dynamically extend Java abstract classes. there’s only gen-class that requires AOT compilation, so basically no-go for any proper REPL-based development. there’re two proposed patches that would make it easier, but they’re both stale for last 5 years
https://clojure.atlassian.net/browse/CLJ-1255
https://clojure.atlassian.net/browse/CLJ-2343
so we’re left with using Java interfaces + classes, but without abstract classes there’s no gain over Clojure protocols + deftypes. and Java interfaces do not allow mutable fields, so yeah. I’ll look around is there a userland solution for that, in the meantime I’m trying to teach clj-kondo how to parse deftype+
I’m not sure. (.child AWrapperNode) this reads accessing method on a class
What kind of editor are you testing this on? You were able to teach it about custom .field meaning?
Or does it just stopped complaining because it looks like reflection call? If I did (.notchild AWrapperNode) will the be a warning?
I’m using spacemacs and it works without additional config. I only added clj-kondo config to treat defparent and deftype+ as clojure.core deftype. If you call this with unknown field it will throw immediately on macroexpansion. Function that replaces parent field access with simple field names is checking whether the field actually exist and whether it’s configured to be a parent field
It’s not perfect, because it looks like field access on a class, agreed
But there’s no static check whether the field exists. You have to call deftype+ for macroexpansion to happen
Wait the editor is doing macroexpansion?
If yes, then I don’t understand why it doesn’t see fields, if it expanded the macro anyway
No, at this point editor is just seeing field access and dont care whether the field exists. So from editor point of view you can put any name of the field, sadly. The check happens on macroexpansion, so if you put field that doesn’t exist it won’t compile
So I didn’t work out yet how to make editor check it before compilation, but at least it checks on compile time
But with clj-kondo hooks maybe it’s possible to actually make editor check it, I think there’s macroexpansion available there
I’ll try to move this check to clj-kondo level
Okay but error on macroexpansion you’ll get even in my version. What are the upsides here?
Upside is that editor doesn’t complain about reference to non-existing field
Ah, because child is checked but (.child this) isn’t. Got it
What if ANode/ATerminalNode/... will be implemented in Java?
Good question, I can try that tomorrow. Also probably depends what constructs would those be in Java? Interfaces, classes, abstract classes?
I guess all of them. Starting from protocols that will have to become interfaces. I don’t want to depend from Java to Clojure
Implementing them in java may be a PITA for clj-kondo people ... I'm not entirely sure. Speaking of kondo, I saw some basic config for it in the humble repo, but using that config most code is showing errors still - is that an ongoing effort or could I have been using it wrong?
some errors are surely not covered with this config, like referencing parent fields in deftype+ . other than that I needed to copy this config over to my local config to make it work properly
re: clj-kondo, I did it once in the past but didn’t pay attention since
clj-kondo should be already working with java, no? Hard to imagine it not knowing about it, I bet almost any Clojure codebase has some level of jvm interop somewhere
That would actually look like (.-field this) btw so kondo should be happy
reify, no?
reify no, maybe proxy
I’ll see how proxy works with mutable fields from abstract classes
Oh yeah right proxy
ok, proxy works well with abstract class and you can make public fields and access them with (.-size this)
what about inheritance then? if you base it on Java abstract classes I think you lose possibility to give Clojure consumer defparent. you can implement new class from abstract class, you cannot create new abstract class though
but yeah, proxy works ok 👍 but fields need to be public, or protected with setters/getters, because there’s no access to protected fields via proxy
As I imagine it, there’s not much need to implement new abstract classes
All this time I had four and it seems to be good enough so far
If needed, can always go to java :)
true actually
if it’s not something that has to be done often, than you can write it in Java and if somebody wants to add one they can always recompile
It would actually feel pretty good to get rid of that hacky defparent thing and switch to well-understood java
Sounds easy enough. I can rewrite it someday this week
With abst classes and Clojure proxy
Can you check if proxy works well with code reloading in REPL? If yes then go ahead
Sure. but we’re speaking about reloading only Clojure code, right? because I don’t think reloading Java sources will be easy
Yes