for last few days I’ve been thinking a lot about this https://tonsky.me/blog/humble-objects/
this is tough. inheritance? why inheritance? I think Clojure’s first idea is to avoid inheritance (maybe second, after data being immutable by default). wouldn’t some protocols with extend for creating types be enough as a basic way of creating components? I know, what about future-proofing and giving other programmers default implementation? shouldn’t be a problem. default implementation can just be a map, as presented here https://clojuredocs.org/clojure.core/extend with DefaultBaz example. it’s elastic, easy to change, and default implementation is possible.
second: IComponent protocol is huge. implementations of some methods are basically no-ops, like -event and -event-impl , or -unmount and -unmount-impl on ANode . it looks like it could be split into smaller protocols: Measurable, EventHandler, WithContext , WithChildren etc (names created on the spot). then it should be easy to create default implementations that others can use, but also give them possibility to, easily, mix and match with their own implementations. and there’s no need to provide no-ops.
also, if there’s -measure and -measure-impl on one level, it looks like there’s few different types trapped in one. maybe more elastic way would be to keep -measure on component level and have another type, NodeMeasure that implements only -measure-impl and is then provided to Node like (Node. (NodeMeasure.)) so Node in -measure can rely on NodeMeasure to provide -measure-impl without having to provide its implementation itself. also NodeMeasure can then grow on its own, if it’s needed, and doesn’t have to pollute main Node protocol/implementation.
the last thing that seems really java’esque is binding state together with behavior via merging deftypes fields. is there a huge gain from this, over let’s say, providing volatile or atom map as default state source? or, maybe, let’s go step further, and create ComponentState protocol with set! and get methods and use this as a basis for keeping state, allowing consumers to provide different state implementations?
I understand you may prefer direct access to state object fields for better performance than volatile/atom map gives. Then ISettable is stil usable. You can take implementation out from deftype+ and create defmutablestate macro that would return type with mutable fields and -set! method. whole idea of merging type fields might stay how it is, it doesn’t have to be bound with behaviors though and doesn’t have to be the only available option.
Allowing consumers to provide custom state sources, hidden behind ComponentState protocol, allows people to do crazy things like keeping state of all components in one atom and building devtools or visualizing whole component structure based on that. Probably stupid idea for a production app, but for dev mode and exploration, for being able to inspect state of particular components, why not? maybe crazy, maybe stupid, maybe both. but you still keep the ability to provide implementation based on deftypes with mutable fields as a default one for Humble components. you just add additional possibility of customizing it, or providing different version
yup. to reiterate. @tonsky I’m surely not asking you to take a week off and rewrite your lib, because I have a vision. I might have a go at some point and try to create usable system according to my own description. maybe then I discover that your version is better. also, there’s a possibility that some of my ideas make sense and some are pure bollocks
I wrote all this, because I think Humble is awesome, but some parts seems a bit odd for Clojure and that might make it bit harder for people to contribute. it kinda was like that for me. I just wanted to create horizontal scrollbar component, then I started to dig into the code, then I’ve reread your article about Humble objects, then I’ve started to think about it and it kept bugging me for days so yeah. so I came here to write it, hope it’s a proper place. can also move elsewhere if it’s not.
as I said, I’m happy to have a go and write the system I described myself, but first I wanted to get it out and, possibly, get some critique from lib author, and other channel members. if similar discussion has happened somewhere already, please link me to it.
for some reference I’ve been going through this discussion, as they seem to tackle similar problems: https://groups.google.com/g/clojure/c/mr-o9sRyiZ0
all the best 🫡
Thanks for looking at it! It’s very interesting hearing a second opinion that goes into such detail. Re: OOP, I don’t think it’s inherently bad, and it’s certainly a good fit for mutable DOM-like tree. After all, OOP was invented for GUIs. I think replacing it with maps will just make everything messy. I remember people complaining about LightTable because there was no structure to anything, just maps everywhere. Also I don’t think it makes sense to split IComponent into smaller chunks, since every component needs all these methods. deftype+ is certainly not something intended for general use, so it know all about HumbleUI needs. I don't expect people to use it to implement their own classes except for HumbleUI components. State there is not the state of user program, it’s more like bookkeeping. I don’t think it makes sense to abstract it away
cool, I see. regarding inheritance I also don’t think it’s inherently bad. my observation is more from perspective of Clojure dev, trying to add some new component to the lib and then seeing a, kinda, new OOP inheritance system with merging deftype fields. so it’s just take a bit longer to get around because it’s not something I’ve seen before in Clojure. also Emacs and Zed are not able to understand the whole code without help (I’m still working on it) so it’s a bit harder to just open the library and start hacking around I fully get that component state is not user program state and I understand the argument about maps being too open. sounds legit, probably not everything should be an open map. still, some easy way to dump component state and inspect exactly what happens there seems tempting. just to understand the system, as the newcomer, who didn’t built it, without putting prints everywhere although, maybe I can just hack around deftype+, or ISettable and achieve similar thing, without changing how whole system is implemented
The fact that fields are declared in parent but then “magically” accessible in child feels not ideal to me too. And that IDEs don’t know how to work with deftype+. But I really don’t see an alternative. I was actually thinking yesterday that some of components might end up in Java rather than Clojure. I’m seeing a lot of overhead with e.g. arithmetic in profiler
I definitely want users writing low-level components as well, but less often than ui/defcomp-style ones. So in the end it might be some mix of Java and Clojure
yeah, I see that
I was even thinking, when I was reading your article, that it looks like writing some parts in Java might even be easier 😅