Phoenix vs Rails: Mix and Rake tasks
Mix is an equivalent of Rake for running project tasks. But it's much more than what you came to expect from Rake.
Note: I'm focusing here on task writing/running experience in Phoenix and Rails, but please keep in mind that Mix is actually a part of Elixir itself, just as you should remember that Rake can be used outside Rails. So points from this article do apply to Elixir and Ruby themselves as well.
Task naming
Recently, I wrote a comparison of naming conventions between Elixir and Ruby. In that article, I've emphasized how much I like Elixir's convention of per-app module namespacing. This improvement over Ruby has also made its way to the Mix task system and it's even more welcome here. It also helps to avoid conflicts between packages and makes it easier to go for umbrella projects, just like it's with namespacing modules. But that's not all.
I can tell from experience that task naming in Rails is nothing less than pure chaos. I've seen cases where two or more conventions were introduced in a single Rails project, like this:
lang:bash rake db:seed:admin rake notifications rake my_project:accounts:remove_unused
In the first case, a variation of default seed task (rake db:seed
) was made and, just because it fits the same problem domain, it was put into original one's namespace. Second is just a plain name of action or background job, without any namespace. And finally, third one namespaces a task both by project name and by the business domain.
See? Chaos. It's hard to consider these three as parts of the same app even on their own, so how could you hope to do that looking at a complete list of tasks returned by rake -T
command?
I'm not saying Elixir and Phoenix will invent best module and task structure for you. But at the very least you'll see in the task name which package or application introduces it. Who owns that db:seed
task, anyway? Also, you won't be tempted to put your own tasks into namespace owned by a separate package. Phoenix developers could've been, like in this case:
lang:bash mix ecto.gen.migration mix phoenix.gen.model
But even though both tasks generate similar stuff, each is a proper member of its package's space.
One mix to run them all
Being properly per-app namespaced, Mix becomes a perfect candidate for one and only command runner. Sort of a "one ring to rule them all" of the Elixir world. And that's exactly the case. You won't find random per-package commands here, unless the package really is a command line tool at its core.
In Rails, you get to use bundle
, rails
, rake
, sidekiq
and many more executables in each project, each with its own syntax, options and actions. That's just more work for you. For instance, if you want all of them to run in the same way, you'll have to make sure that their binstubs are setup correctly. Rails devs have already noticed that it's weird to have half of Rails tasks under rails
and other half under rake
. But they won't rectify the whole ecosystem.
In Elixir, mix
is your go-to command for attending it all. From creation of a new project, downloading and managing its dependencies, running generators or seeds to running a server and creating a release.
Task code
Rake introduces its own DSL for defining tasks. It's based on namespace
and task
blocks. My experience shows that this makes it hard to organize the growing task code. You could create lots of small "private" tasks invoked by the main "public" task. Or you could place Ruby classes, modules and methods randomly in between the task definitions. Neither option seems to be clean, native and convention-driven.
In Elixir, Mix task is just a module with a run
function, so you get to organize your code the same way as usual. You can split it into functions and modules without having to invent a convention. Also, as Elixir already has a first-class support for documenting code, there was no need for DSL for that too. Instead of desc
method, you can use a native @shortdoc
in order to give your task a description that will appear on the task list. Neat! The best DSL is no DSL if you ask me.
Finally, it's not even necessary to use Mix tasks for one-time execution of code from arbitrary script in the project environment. Phoenix facility for database seeds is a nice example of just running plain script with mix run path/to/script.exs
. It's a bit like rails runner
, but with emphasis put on running files instead of inline code. Also, being used for Phoenix seeds, it feels more like a first-class member of the ecosystem.
Command line arguments
Here's the syntax for passing arguments to Rake tasks in Rails:
lang:ruby # in rake task task :my_task, [:arg1, :arg2] do |t, args| puts "Args were: #{args}" end
lang:bash # from command line rake my_task[1,2]
In your code, you're forced to define your arguments in a weird syntax, which is nowhere close to the usual Ruby method definition experience. It's even worse on the shell end. Necessity to wrap arguments in parentheses and to separate them with commas completely ruins the native command line argument passing and wrapping. Among others, this makes it impossible for your tasks to be on a receiving end of xargs
or any other Unix shell pipeline.
How does Elixir, Mix and Phoenix compare? Here's an example:
lang:elixir # in Mix.Tasks.Github.PullRequest def run("master") do Mix.shell.error "Don't create pull requests against master." end def run(branch) do url = create_pull_request(branch) Mix.shell.info "Pull Request ready at: #{url}" end
lang:bash # from command line mix github:pull_request staging
Your tasks are just modules with a run
function. This function simply receives command line args. Therefore, you can easily pattern match against them, like in example above. And so your experience on Elixir end is native. Same for the shell. Simple and beautiful.
Finally, I'd like to point you to Mix.Shell documentation which lists all the handy functions that you can use when writing command line tasks. It covers running commands, escaping their arguments, writing console messages (like in example above) and prompting for input.
Summary
When it comes to writing and running tasks, Phoenix seems to be even more convention-driven than Ruby and Rails. It greatly improves upon Rake's naming conventions, executable creation conventions and handling of the basic pieces of command line universe, like arguments and I/O.
Also, this is nothing more than Mix built into Elixir itself, so you'll have the same experience no matter if it's a Phoenix project, OTP/umbrella project or just a simple one.