Phoenix vs Rails: Mix and Rake tasks

SeriesPhoenix on Rails clock6 min read

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.