I still don't understand what the "task abstraction" is or what it provides. It seems to me that it's simply a Clojure function with a corresponding command line interface. Is that fair? Does it do something else too?
If it's just a function, I don't know why command-line support is valuable. For interactive use, the Clojure REPL is just fine. For automated use, you only need a single shell utility like perl or awk to evaluate an expression or run a script.
Yes, in the post I didn't get too technical with the treatment of tasks; I'll elaborate a little here. First, the command line thing.
You're right that the command line isn't strictly required to use boot, you can do everything at the REPL or perl/awk etc., as you pointed out. But for me it's really useful just ergonomically to be able to use command line arguments to configure ad-hoc builds because they can be very concise. Just like I probably wouldn't be super excited to use a Clojure shell instead of Bash, because Lisp is usually more verbose for the kinds of things I do on the command line. Consider:
the command line version is just nicer for that. When it's time to automate is when you'd put that in your build.boot and make it a new task.
This brings us to tasks. We used to describe them as "middleware factories" but Rich has provided us with a way cooler name: stateful transducers. The build process can be imagined as a transducer stack applied to a file set instead of to an async channel or sequential thing. The principal value the task abstraction provides is their process-building power.
A typical task definition looks like this:
(deftask foo
"This task does foo."
[...] ; kwargs/cli-opts
(let [state ...] ; local state
(fn [continue] ; middleware
(fn [event] ; handler
... ; build something, do work
(continue event) ; call continuation
))))
Please ignore "event", it's there for historical reasons. But like transducers, we have powerful ways to build processes from tasks that don't need to know anything about each other now. For example:
A key property of transducers is that they can also perform process control flow duties. The boot `watch` task, for instance, is a totally general-purpose way to to incremental-anything in boot. The `cljs` task doesn't have a file watcher in it, none of the other tasks do. They don't need it.
Another example is the `cljs-repl` task, which emits ClojureScript code when you start the CLJS REPL. This requires recompiling the JS file and reloading the client. This all happens automatically because the cljs-repl task can call its continuation whenever it likes, so it does that when you start the REPL. This means that your webapp code doesn't contain any REPL connecting code, so you don't have to think about removing it for production builds etc. The REPL connecting code is in there when you use the task, and not when you don't. Very clean.
Another interesting property of tasks is that they accept only keyword arguments. They do not take positional parameters. This means that partial application of tasks is idempotent, and that last-setting wins. For instance, given a function f that takes no positional parameters, we have:
(-> f (partial :foo "bar")) ==
(-> f (partial :foo "bar") (partial :foo "bar"))
and
(-> f (partial :foo "bar")) ==
(-> f (partial :foo "baz") (partial :foo "bar")).
This is pretty interesting because it gives us a nice way to manage global preferences. We have a macro called task-options! which can be used to globally apply options to tasks:
This macro actually does some currying and alter-var-root, replacing the value of the task var (deftask defines a var, of course) with a curried version. A cool thing about this is that the last-setting-wins property means that you can override these settings on the command line or in the REPL:
(boot (foo :bar "not-baz") (omg))
which would override the :bar option, but not the others.
This is probably long enough, hahaha! I'll hand the mic back to you now :)
> it's really useful just ergonomically to be able to use command line arguments to configure ad-hoc builds
It's largely also what contributes to "works for me" build environments... It's better to have a just one way to do it interface and discourage excessive tinkering with parameters. The more parameters, the more likely for your dev env to be unstable across individual checkouts or developers. I know it's idealistic, but I think we should strive for zero-arg builds, which oddly means not making it easier to configure them.
I'll have to think on all the other stuff you wrote, since it's not totally clear to me yet. I may ping you again after I noodle a bit.
Your point about repeatability is valid. As a policy matter we would never advocate building a project without codifying the process as zero-arg tasks in the build.boot file. You can see this in our own projects (eg. https://github.com/tailrecursion/boot-useful/blob/master/bui...). This project has one way to build the project jar file:
But we don't only use boot for repeatable builds! Boot is in a unique position in that it's on the intersection of application and environment. That is to say, boot can be used to "bootstrap" the application. With boot we can create sort of self-configuring applications, where the entry point of the application is the build.boot file. This is a very clear win, for example, when running Clojure applications in docker on Elastic Beanstalk, etc. (We'll write that up, too.)
If it's just a function, I don't know why command-line support is valuable. For interactive use, the Clojure REPL is just fine. For automated use, you only need a single shell utility like perl or awk to evaluate an expression or run a script.