Hi Jeff,
What do you think about a Map interface for this?
I recently implemented something similar in a project of mine, which I called an 'evaluating-map'. It's not a Web project, but the pattern is a general one. In my case, a DSL for specifying a job to run. I want the DSL writer to have access to a lot of data/logic which can come from a lot of different sources (a "big ball of mud" to use your term).
Like you, the mud-ball could contain values or functions. These functions can have references to other values in the "ball of mud". I expanded this to include interpolated strings (e.g. "foo is ${foo}") and collections of values/Strings/functions which are handled recursively. My code doesn't do anything to manage state, although users are encouraged to provide memoized functions and a helper is provided to assist with this.
Here's an example comparable to your example from the Kiln project.
(def m
{:request "foo"
:uri #(build-uri (:request %)) ; an anonymous function works, or
:path (lfn [uri] (.getPath uri)) ; use lfn, a helper that returns a fn
:dispatch
(lfn [path] (condp = path
"/remove-user" :remove-user
"/add-user" :add-user
"/view-user" :view-user))
:action! action ; assuming action is defn'ed elsewhere
... and so on ... })
lfn is the helper that I mentioned-- it pulls it's args as keys from the "ball-of-mud" and returns a memoized fn of those args.
Eventually, you fire it. Like Kiln, the concept is that you have a bunch of code that sets it up and then at some point you mix in a few seed values and kick it off. My fire function does a bunch of other stuff, but the relevant part boils down to (-> m (assoc :request req) evaluating-map), which is used like this:
(let [k (-> m (assoc :request req) evaluating-map)
result (try
(:action! k)
(render-template (:template k) ...other kiln data...)
... catch)]
; because it's a Map, you can do things like
(log/debug (select-keys k [:uri :path]))
result)
I didn't write support for glazes and cleanup. I think glazes could be done ring-style. Cleanup requires some extra thought. Those are nice features of Kiln.
I think Kiln gives you more control over the execution and state, making things like cleanup easy. What I like about the Map interface, aside from the convenience of being able to use standard collection functions (merge, select-keys, dissoc, etc) is that you can construct the map from many different sources. I.e. you can merge maps which are constructed dynamically at different points in your flow. This was important for my use-case, since DSL users are writing code that my core code knows nothing about. Using the example above, a subsequent user could replace the :uri fn:
(merge m {:uri (lfn [request] (some-other-build-fn request))})
This new function would then be the input for the :path function.
My code for this abstraction isn't isolated, but you can see it in context of another project
here.
Anyway, I like the project and thanks for sharing it.
Marc