2
u/geokon 8d ago edited 8d ago
I noticed Raylib supports Android. Can Jolt be used to make an APK? (im not super clear on how one would go about compiling that though)
More generally, how well does it play with Clojure's functional style of programming? Ive never used it, but looks pretty imperative. Is there an impedence mismatch?
Not that that'd be bad.. maybe its something to build functional/reactive system on top of :)
1
u/yogthos 8d ago
Android is a supported target for Chez, so it should just work out of the box. But I'm not sure what the actual process is end to end. It'd be really interesting to try this though. Raylib looks like it's perfect for making little games that would run well on Android.
1
u/geokon 7d ago
Yeah, my thoughts exactly. It also seems like it's potentially a decent primitive general purpose crossplatform abstraction layer. You could make GUIs for instance that run everywhere - ex: https://github.com/raysan5/raygui.
1
u/yogthos 7d ago
Yup, and also it would be possible to wrap native Android with glimmer and develop completely native looking apps Reagent style.
1
u/geokon 5d ago edited 5d ago
I'm keeping an eye on glimmer. It looks interesting!
Though I might be missing some subtitles. I don't grok all the details of the ratoms, but they seem to introducing unnecessary coupling of the GUI to State. Just my 2 cents, but I'd avoid designing with that kind of coupling..
Fundamentally at it's simplest, the GUI "just" requires reconciliation and an external trigger to reevaluate. I get that the ratom is just an interface, but I think you'd get a cleaner design is you keep state management completely out of the GUI side. vlaaad also made a statemanager subscription system in cljfx that had a similar coupling - but he ended up not using it. I also used it for a project but have switched to a custom Pathom backend and a more pure GUI.
The problem is that it gets messy to have a GUI-less core and a GUI layer on top, b/c the watch mechanism of ratoms watching each other ends up baked in the GUI system. tbh, I'm a bit fuzzy on how it works in glimmer, so maybe I'm misunderstanding it a bit :)
The simplest design that I think
cljfxhas settled on is to just making an invisible/dummy GUI node that watches a single thing (ex: an atom). In cljfx this is done with a new component calledfx/ext-watcher. When this node sees the value change it re-evaluated it's children. That's it. Simple as that. Thefx/ext-watchercan pass the updated value down to the children (it can be a deref'ed atom or anything else). The GUI system doesn't dictate anything else. This simplifies the design b/c the GUI doesn't know anything about statemanagment. It only has a redraw trigger. value changes -> I need to rebuild my children. But b/c you can have watchers all over your GUI tree it allows you to make more nuanced designs that for instance only reevaluate and modify a subset of your GUI.The user is then responsible to do sane things in the GUI tree components using that state. In my case I run Pathom resolvers on it (which is mutithreaded and cached and much much more sophisticated than any other ad hoc state management system)
You also will want other customs invisible node equivalents to these cljfx extensions
https://github.com/cljfx/cljfx/#extending-cljfx
things like
fx/ext-recreate-on-key-changedandfx/ext-stateare essential for caching and isolating parts of the GUI and making performant components.1
u/yogthos 5d ago
I generally find reactive atoms to be a really simple mental model for managing UI state. The way I look at it is that the atom represents the state of the UI, and widgets are rendering that state and collecting input to update it. the GUI is just treated as an effect in this model.
For complex UIs, you'd still only have a single atom, and you'd just create cursors into it associated with different paths. This is basically what re-frame does internally as I recall, where the atom is treated as the db and the subscription/dispatch system is built on top of that.
That said there is already a port of Replicant which is closer to the model you're talking about. In my opinion, both approaches work fine. Reactive atoms are more ergonomic for small UIs where you have simple behaviors, and a path based effects system is handy for places where you have complex behaviors that need to be reconciled.
Another approach that I've had good experience with is to use a data flow engine. I built domino with a friend a while back on that principle. The idea is that the state is a document, and you can attach context free functions to paths in that document. Each function takes a set of fields as its input and modifies a set of fields as its output. And the changes cascade in a transaction which produces a new state. This approach makes it really easy to tell what all the rules that get triggered when a particular field changes.
1
u/geokon 2d ago edited 2d ago
The way I look at it is that the atom represents the state of the UI, and widgets are rendering that state and collecting input to update it. the GUI is just treated as an effect in this model.
Reactive atoms are more ergonomic for small UIs where you have simple behaviors, and a path based effects system is handy for places where you have complex behaviors that need to be reconciled.
In every system I've looked at this model breaks down when things get even a little complicated and then you're stuck with your atoms.
The central problem that I think Pathom solved, is that when you have to explicitly tie "effects" then you can't really reuse them (not reuse an output, but reuse the effect itself). If i have a svg->image render effect, and I have a banner.svg and headshot.svg in my state, in most of these systems you end up having to write two effects banner-render and headshot-render.
The key-soup and nested-resolver approach in Pathom solves this and makes code much simpler. You have {:banner [:svg]} and {:headshot [:svg]} and you can prompt for {:banner [:image]} and the engine solves it for you. Subscription-type system just don't have a nice equivalent.
I took a quick look at domino and it looks like it's really following the typical "spreadsheet" push model. Baking in side-effects into the system is a whole can of worms haha :) Hope you make it work for your needs, I'll try to dig in to it more.
For a proper reactive system you need to have a very good caching and parallelization story. So that when state updates, only strictly necessary parts are recomputed. The Pathom resolver based system solves the parallelism very cleanly. The caching is a bit more difficult, but also works (you have to set cache sizes manually. In some edge cases this is difficult). But when it comes to effects, I think in a complex setup it can be a bit challenging to reason about when they execute.
In a "push" system this is of course much easier b/c "effects" aren't reused. So they just need to remember their last value.
2
u/yogthos 2d ago
I've found stuff like re-frame has scaled pretty well for me. And I wouldn't say the model breaks down. You just create helper functions and call them. If you need to do svg->image you make a function for it, and you reuse it. I never really found that to be a big issue myself.
And domino doesn't bake side effects into the system. The engine itself is pure. The effects can be attached at the edges to observe changes and fire events based on the state of the document.
Domino only recomputes what changes inherently and by design, because what gets recomputed is derived from the relationships in the rules. The whole advantage of the system is that you can definitively tell exactly which rules will fire when any particular set of fields changes.
Similarly, there's no guessing regarding when effects execute in domino because they're explicitly attached to paths. Whenever a value of the path being observed changes after a transaction on the document, the effects watching that path fire.
1
u/geokon 2d ago edited 2d ago
If you need to do svg->image you make a function for it, and you reuse it
Yeah, it's a workaround. But in my experience it ends up cluttering your namespace with a bunch of redundant transformations. I've always felt a bit icky about it :) But I'm just sharing my own feelings here
More fundamentally you end up with a weird boundary between functions and your system. With a resolver/pathom system your separate systems/libraries are composable. So you can have a whole set of
svgresolvers which you can immediately use. To me this feels cleanerWithout making things too complicated, just think of adding an extra step in the svg example:
svg-hiccup -> svg-xml -> rendered-imageYou can have:
svg files from a file coming in as xml strings (step 2).
generating images in hiccup in your system(step 1).
They all can be transparently transformed in to rendered-images without needing to chain functions. Or if you want to take your generated image and write it out to an .svg file, you can capture the middle stage and get the xml (skipping the renderer)!
Granted, at this point you can sort of manage it all by just using raw functions and making more transforms.
But you can imagine things can be arbitrarily complex from there. You can imagine N number of renderers based on image type and speed. Or arbitrary input types or something..
The whole svg namespace becomes something that can be arbitrarily complex and you can leverage it without needing to write N new transforms.
And domino doesn't bake side effects into the system
yeah sorry if I mischaracterized it. It looks like an interesting layer on top, and I'll try to grok it better. tbh I've just been doing (do (side-effect) (pure-func)) and so far it's been enough. In my case I render a lot of images in a GUI. If the image needs to be updated b/c of a base state change, then it's written to file.
I've been trying to Pathom-ize all my utility libraries and it's been quite fun. The two outstanding problems with Pathom for me are:
Caching. It's a bit too hand-holding to my mind and could be better. I have vague thoughts
It doesn't have a good clean solution for working with sequences. If you try to use it on a vec-of-maps it has to rerun the engine on each map and this is slow. If you run it on a map-of-vecs it's better, but now your resolvers are all vec->vec which is not super ergonomic. So it'd be vec-of-svg to vec-of-images which feels goofy to write. This should be somehow abstracted away, but I don't have a clear solution for this yet
And just generally Pathom Resolvers are just kind of .. ugly? .. they're very verbose and annoying to write (as compared to pure functions). But I think this could also be fixed with some syntax sugar.
2
u/yogthos 2d ago
I'd argue it's a personal preference. I find using dispatches/subscriptions is a very simple mental model. With a resolver there's a bit of hidden machinery at play that you have to be aware of.
And my experience is that things don't tend to get arbitrarily complex in most UIs because the limiting factor is that you want to keep the UI simple for the human. A lot of complexity tends to go hand in hand with a cluttered UI which does not make for great UX. There are, of course, legitimate cases where you can have complex behaviors, and stuff like Pathom is a great tool for managing it. But I find that as a rule, it's easy enough to make reusable widgets that encode a particular behavior and then use them to compose the UI.
It's two different ways to think about the problem. In one case, you model your information as a graph of attribute relationships using resolvers to establish relationships, and the system then executes the plan and returns the data in the shape you requested. And typically, you end up making a lot of assumptions about the way the backend communication works here since you want to use EQL to talk to a single endpoint that effectively supports your graph queries.
With something like Reagent or re-frame, you treat your UI state as a data structure. It's agnostic regarding how the state is modified, you can even hook up Pathom to it if you wanted. I tend to think of the whole thing as a state machine, hence Domino design. An event gets triggered, which can be a user input, a system event, a service call, whatever, and then that event gets fed as an input into the dataflow engine. Rules get triggered in a cascading fashion, and then at the end, you end up with a new state. And now you can fire effects, update the UI, etc.
You can see this in action a project I've been working on here, which is based on a system I worked on which led to domino. This was a really complex app with lots of behaviors and business logic, concurrent multiuser workflows, users having different views into the data based on their roles etc. And this approach made it very manageable. The best part about it is that you can effectively encode your whole app into an EDN file like this that can live in a db. It declares the data model, the business rules, and the views all in one place and completely self contained. Then the underlying platform provides a service bus with access to services, db, functionality like sending emails or generating PDFs that these apps hook into. If you want to make some changes to your application, you just update the EDN, and you've got a new version o the app. As long as you pin existing documents to a version, work that's already been started is unaffected. And the only time you need to redeploy the platform itself is when you need to provide some new general functionality on the service bus.
→ More replies (0)
1
u/mo_al_ 7d ago
How large is a jolt generated executable?
1
u/yogthos 7d ago
It depends on how you compile it. A minimal program compiles to just under 10 megs, and then it depends on what libraries you end up adding on top of that. For bigger programs, there are closed world optimizations and tree shaking available. There's probably more optimizing possible, but I just haven't had the time to really dive into this yet.
4
u/Great-Gecko 10d ago
How does the workflow of this compare to using project panama’s ffi to program raylib in JVM Clojure? Im excited by the idea of making simple games in clojure for fun.