Context
I've was an experienced Clojure programmer when I started using it. I was already familiar with both Clara Rules and Precept. I actually built a project that used Clara to render hiccup components. All of my experience in using O'doyle is based on building Clojurescript Single Page Applications. I also gave a talk about it in 2023.
I started by building a handful of experimental projects [ a scheduler based on genetic algorithms, a todo list (source) and a deck of affirmations (source) ]. Then I began the work on mypoetryjournal.com that would carry forward to this day.
My Poetry Journal started as a "daily prompt" website, that evolved to display poems where the lines would be highlighted in time with an audio recording. Then that evolved into a simple text editor for poetry.
After some time I refactored and built Giant Heart Poetry which integrated with firebase for cloud storage and serverless functions. It also included client-side encryption, full-text search (using interop) and tags. All of these features were built up incrementally over the course of two-ish years of part-time work. [You can see an intermediary step with local-only storage here]
The biggest strength of O'doyle was that the skeleton for adding/removing features was already built in. For a long time I could make changes to the codebase without worrying that things would break.
Challenges
- Creating facts with numerical ids where you have to manually figure out what the next-id is can be a pain. I eventually switched to using (random-uuid) keywords when I needed to create facts like that. Definitely less memory efficient, but it's more convenient for either db storage, or storing the serialized session locally.
- I ran into performance issues when trying to read and process > 1000 db results. It's possible to have un-needed rules trigger when you're just trying to hydrate the session. This might not be applicable to your application, but because I needed to have all the poems downloaded, decrypted and indexed to perform local search I encountered it. My solution involved adding some locks (that would prevent calculating derived facts until all data was read for eg.) But as soon as I started doing this my code became ossified and I lost a lot of flexibility. Maybe this is less of an immediate concern on the JVM, but having all rules fire on every fact insert by default can lead to performance bottlenecks. I was able to detect them by using the performance tab of the browser dev-tools which would show you which of the rules were taking up the most compute time.
Surprising wins
- Serializing the session means that we can create easy save-files by storing a selection of our application state and then loading that state into our system. (I have some helper functions for doing this while migrating between schemas if anyone is interested)
- The spec integration is elite. You should use it because it catches the most common pitfalls.
- Being able to query rules to see the facts that trigger (or don't trigger) them is very helpful and you can use it from the REPL when you're stuck.
- Using namespaced keywords for fact definition, as in:
[::global ::enable-markdown? true] in the example.configuration namespace, and then referencing it as [::configuration/global ::configuration/enable-markdown? enable-markdown?] when you need it somewhere else helps with organization. It allows you to split your rule definitions across multiple files with an encouraged dependency flow. If you absolutely need an escape hatch without causing a circular dependency error, you can use the fully qualified keyword directly, as in: :example.configuration/global
Opinionated Approach
The strength is that it's a very flexible tool, but the challenge is that if you're not intentional, you can still wander into a clumsy architecture.
A mistake I made was creating an "events" namespace that my orum render components would call instead of manipulating globals directly. Good in theory, but difficult in practice because I ended up with a very tangled and large events file.
About 8 months ago, I re-evaluated my architecture rebuilding the project without using O'doyle a few times, and then I settled on the following approach.
- I render components based on
::global or ::derived facts
- If a rendered component needs to cause some side effect, it does not manipulate the global state directly, it triggers a
::request rule, such as
(insert! *session ::configuration/request {::configuration/toggle-markdown true})
and then I'll have a rule that looks like this to handle it:
::toggle-markdown
[:what
[::request ::toggle-markdown true]
[::global ::enable-markdown? enable-markdown? {:then false}]
:then
(o/insert! ::global {::enable-markdown? (not enable-markdown?)})]
This means that I can wire up components in a standard way without needing to pass a bunch of extra state to them because the actual changes happen in dedicated namespaces.
You can see my most recent implementation @ paperbalm.com
Context
I've was an experienced Clojure programmer when I started using it. I was already familiar with both Clara Rules and Precept. I actually built a project that used Clara to render hiccup components. All of my experience in using O'doyle is based on building Clojurescript Single Page Applications. I also gave a talk about it in 2023.
I started by building a handful of experimental projects [ a scheduler based on genetic algorithms, a todo list (source) and a deck of affirmations (source) ]. Then I began the work on mypoetryjournal.com that would carry forward to this day.
My Poetry Journal started as a "daily prompt" website, that evolved to display poems where the lines would be highlighted in time with an audio recording. Then that evolved into a simple text editor for poetry.
After some time I refactored and built Giant Heart Poetry which integrated with firebase for cloud storage and serverless functions. It also included client-side encryption, full-text search (using interop) and tags. All of these features were built up incrementally over the course of two-ish years of part-time work. [You can see an intermediary step with local-only storage here]
Challenges
Surprising wins
[::global ::enable-markdown? true]in the example.configuration namespace, and then referencing it as[::configuration/global ::configuration/enable-markdown? enable-markdown?]when you need it somewhere else helps with organization. It allows you to split your rule definitions across multiple files with an encouraged dependency flow. If you absolutely need an escape hatch without causing a circular dependency error, you can use the fully qualified keyword directly, as in::example.configuration/globalOpinionated Approach
The strength is that it's a very flexible tool, but the challenge is that if you're not intentional, you can still wander into a clumsy architecture.
A mistake I made was creating an "events" namespace that my orum render components would call instead of manipulating globals directly. Good in theory, but difficult in practice because I ended up with a very tangled and large events file.
About 8 months ago, I re-evaluated my architecture rebuilding the project without using O'doyle a few times, and then I settled on the following approach.
::globalor::derivedfacts::requestrule, such asand then I'll have a rule that looks like this to handle it:
::toggle-markdown [:what [::request ::toggle-markdown true] [::global ::enable-markdown? enable-markdown? {:then false}] :then (o/insert! ::global {::enable-markdown? (not enable-markdown?)})]This means that I can wire up components in a standard way without needing to pass a bunch of extra state to them because the actual changes happen in dedicated namespaces.
You can see my most recent implementation @ paperbalm.com