I am exploring Clojure macros to eliminate boilerplate when composing complex, nested workflows.
• Workflows: Functions that process a "threaded map" through a series of steps using a loop/recur pattern.
• Namespacing: To prevent key collisions, I use qualified keys. However, some workflows share keys (e.g., :shared-ns/name), complicating state management.
• State Management: I’ve moved toward using an atom for options to handle nested workflows. If a top-level workflow myns/walter contains sub-workflows myns/ansible and myns/tofu, the options map mirrors this hierarchy: {:myns/ansible-opts {...} :myns/tofu-opts {...}}.
• Execution Logic: The engine must distinguish between a "leaf" step (regular function) and a "sub-workflow." It must also handle "option migration"—where specific data (like an IP address from a Terraform output) is mapped into the input of a subsequent sub-workflow (like Ansible).
The goal of the macro is to create a DSL that accepts a simple data structure representing these
dependencies and automatically generates the wiring, state-shuffling, and namespacing logic.
This is the code that I need to generate with the macro:
https://github.com/amiorin/walter/blob/f7629fad0ea18003852799e7ce4189dc2b5f9ab3/src/comp_wf.clj#L28-L63
Is there something that I should be aware of before trying to write this macro.
I'm trying first with a function.
Yeah, the first step after deciding that you probably need a macro is to forget about it for some time and try with functions instead. :)
The fn is enough. 🎉
The first rule of Macro Club is... 🙂
yeah, start with an fn and see how far you get. generally, the things that make macros nice are when you need a & body param that you don't want to force users to wrap in an anonymous function, or you need information about the environment (line numbers on the called form)
Hi, this is a slightly open ended question. I'm trying to understand what the purpose of :preserve :read-cond mode in read & read-string is. Does anyone have experience where they needed this? Also, once I have the reader conditional, how do I actually evaluate it?
user=> (read-string {:read-cond :preserve} "#?(:default 0 :jank 0 :clj 0)")
#?(:default 0 :jank 0 :clj 0)
user=> (type (read-string {:read-cond :preserve} "#?(:default 0 :jank 0 :clj 0)"))
clojure.lang.ReaderConditional
---
Full disclaimer I'm trying to implement read-string in jank, so please let me know if this type of question is not meant to be asked here.> how do I actually evaluate it?
What do you mean by evaluate? Usually, the goal of read is to turn a stream of characters into data structures without any evaluation occurring.
> I'm trying to understand what the purpose of :preserve :read-cond mode in read & read-string is.
Sometimes, it's useful to treat code as data (eg. code analysis). In those cases you want to represent the code as data.
I suppose my main question would just be what's the potential usages for this feature. Code analysis gives me some hints. Thanks for that! The most basic usage in my mind is evaluation so I assumed if we can read it in while preserving it, there might be a subsequent evaluation stage.
I made a mistake of prematurely reworking the parser and it bit me, so this time I'm trying to understand the purpose first 😛.
I don't think it's actually used that often. You can search for uses of :preserve , https://cloogle.phronemophobic.com/name-search.html?q=preserve&tables=keywords
You can also browse usages of :read-cond, https://cloogle.phronemophobic.com/name-search.html?q=read-cond&tables=keywords.
Many of the usages are just faithfully reimplementing its usage.
I think shadow-cljs, tools.reader, analyzers, etc might use it for analysis.
For many analysis use cases, you also want more info about the actual character stream. For those use cases, folks just use something like edamame or tree sitter (which are alternative readers).
util=> (get (read-string {:read-cond :preserve} "#?(:default 0 :jank 0 :clj 0)")
clojure.lang.ReaderConditional/FORM_KW)
(:default 0 :jank 0 :clj 0)
util=> (read-string {:read-cond :allow} "#?(:default 0 :jank 0 :clj 0)")
0neat
@smith.adriane Thanks for your input, I'll try to check a couple of usages and see if I can gleam other use cases else I have some idea for its usage and subsequent implementation in jank.
dewey also has a sqlite data dump where it's plausible that you could write a sql query to could find all the lines of code that use read/read-string near other lines that have :read-cond, :preserve.
Some day, I'll setup a datalog db where you can just run queries.
I implemented this originally so happy to shed some light. Conditional reading allows you to read in the context of a particular platform and conditionally choose what is read. This is allowed in cljc files, not allowed in clj files etc. in some cases, tools (like an analyzer) need to read and preserve the conditional read construct itself so that it can be re-emitted, evaluated, analyzed, whatever
There are some docs about this in read https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/read
Also https://clojure.org/reference/reader#_reader_conditionals
Great, that helps! I think I'll get the implementation right with this.
I was wondering why there was a TaggedLiteral runtime object when reader suppression was being turned on. It makes sense now. Thanks!