In overarch I use SCI to evaluate Comb templates, so sci/eval-string will get called quite often with the same string (the template code) and different options (same namespace setup and load-fn, but different data). As I now added a load-fn to include namespaces from the "template library" I wonder, what the impact on performance is and if it can be optimized. I'm using a memoized load function so loading the namespaces is cached. But parsing will happen inside of SCI and I suppose there is no caching over the many calls to eval-string?!?
I you need to execute parsed code, you can evaluate a function and call that function over and over?
:load-fn will only be called for namespaces that were not already loaded
For example I have a node.md.cmb template, which is called to generate the markdown documentation for every node in the architecture model (could be 1000 nodes). So 1000 calls to eval-string.
are these strings or already clojure forms?
yes, I parse each template once, so without the load-fn, I was not too worried about the performance.
A or B? yes
A: Strings (currently)
I don't see a way how to avoid parsing those strings then? perhaps caching works for your situation if the evaluations are pure and not stateful
what you could do is cache the parsing and then use eval-form instead
but only do this if you have measurable performance issues
Does SCI expose a read-string function, which could be cached?
yes, look at these three: https://github.com/babashka/sci/blob/master/API.md#sci.core/parse-next
+ eval-from + reader
also using eval-string* is already better since you don't create a new context from the options all the time
so first create a context ctx with sci/init , and a reader (from the string)
then parse the string with (sci/parse-next ctx reader)
or parse-string if you expect only one form
and then evaluate that with eval-form
I'm wrapping the complete template in a 'do' form anyway so there's one toplevel form. Calling parse-next will also load the required namespaces in the ctx or include them in the returned form?
no, parse parses, it doesn't load anything
can you give an example of how your expressions look
https://github.com/soulspace-org/overarch/blob/main/templates/docs/node.md.cmb
the required namespaces will be evaluated when you eval-form the form
makes sense, right?
Yes, I suppose. And it will happen on each call of eval-form?
I don't understand the question
Each of the calls are independent and if there is no state for the loaded namespaces in the context, the require will need to load the namespaces many times.
yes, therefore: use a ctx argument
So the context manages/caches the namespaces?
yes. also if you add those lib namespaces as a configuration on the context (or the options) in :namespaces there is nothing to load either
I distinguish between the overarch code, which is added via the namespace key in options, and template code which could be changed by the user to customize the output of the generation. Overarch is distributed as a uberjar, so the overarch code is closed, but the template code is open and can be customized. I'd like to keep this separation because many users of overarch are not familiar with Clojure (yet).
yes. makes sense. users can reload their code with (require '[foo] :reload) . otherwise it's cached.
https://github.com/soulspace-org/overarch/blob/4f39cb994e083c2ba610958306dcfd934602704d/src/org/soulspace/overarch/adapter/template/comb.clj#L61 this is the current code for executing the templates with SCI.
I really love SCI for providing a sandbox for executing the templates and it's currently very little code necessary to implement a template engine this way. Thanks for that!
As a contrast, this is a template engine I once implemented in Java for model driven software development: https://github.com/lsolbach/TemplateEngine. It's a halfbaked re-implementation of Lisp in a complex and error prone way. I'm glad, we're past this in the Clojure community. 😅
hehe