Avoid these pitfalls when moving from Rails to Phoenix
Elixir, the functional language created by José Valim, has gotten a lot of traction recently. There are more and more companies that use it and recruiters starting to approach me for Elixir specific projects.
The most popular web framework in this language is Phoenix.
I want to give you some guidance on what to avoid when starting an Elixir based web application (using its Phoenix framework) — especially when you have a background in Rails.
Should I even switch to Phoenix?
The first fundamental question you'll have to answer for yourself is: should I use Phoenix instead of Rails?
Both of these frameworks try to achieve the same goal (building full-stack web applications), but still have their differences.
I personally believe that Phoenix is fundamentally better on a technical level than Rails for two major reasons:
It Learned from Rails' Mistakes
Because Phoenix is the newer framework, and the Ruby and Elixir communities have a great overlap, it is only logical that you can take a look what is already there, take the parts you like, and omit the parts that don't feel right to you.
As far as I am concerned, a couple of great decisions have been made, namely:
- Not providing an asset pipeline (by default, this is handled via brunch)
- Making the data layer more explicit (more below)
The Erlang VM is Better Suited for Web Applications
Ruby initially was designed as a scripting language — it was never intended to be used on the web. Although concurrency models have gotten better over time, I still feel it is better suited for running scripts rather than long-running web applications.
Elixir is based on the Erlang VM (BEAM), which has been developed for telephone systems — Erlang's standard library is still called Open Telecom Platform (OTP). Its design had long-running applications in mind and makes them resilient, a paradigm that fits today's web applications very well.
Phoenix has therefore been mentioned in scenarios where scaling to many connections was necessary.
However ...
Rails is NOT Dead
That being said, there are still valid reasons for sticking to Rails. Scaling problems only occur after you acquire a certain user base. Rails will get you pretty far, and there are still a lot more Rails developers out there than Phoenix ones.
Furthermore, the amount of documentation you get when developing Rails web application by far exceeds what is available for Phoenix.
Of course, you shouldn't stick to the status quo just because the market tells you to. Playing around with new technology is always fun, and innovation does not come without risk.
Nevertheless, I came up with three criteria. Try to fulfill at least one of these before starting a Phoenix project.
Low Stakes Project
Is the project you are starting a pet project for yourself or an internal experiment? Or more generally: can you afford to completely rewrite it?
Dev Team Willing to Support You
Can you find at least two other people on your team who want to do the project with Phoenix and are willing to support it at 4 am?
Few Exotic/Outdated Dependencies
Does your project depend only on mainstream databases (Postgre/MySQL)? Are the APIs you're using JSON over HTTP?
Let's get started
Okay, so let's assume you made a reasonable decision to start a project with Phoenix. Be careful — here are some of the pitfalls I encountered that I want you to avoid.
Using Ecto like ActiveRecord
Database access in Rails is handled via ActiveRecord. In Phoenix, database access is handled via Ecto. This is as far as the similarities go. The concepts behind these two data layers are fundamentally different.
To demonstrate the key difference, here is an example in Rails. Assume you have an article model with a relation to many comments.
article = Article.find(id)
puts article.comments.inspect
What will this program print? This is not a trick question. The answer is the comments that are associated with the post.
The equivalent code in Phoenix looks something like this:
article = Repo.get(Article, id)
IO.inspect(article.comments)
Running this code prints out something completely different. It will return an error structure:
#Ecto.Association.NotLoaded<association :comments is not loaded>
Because comments
is an association, it is not automatically returned when you just load the article model. The reason it still works in Rails is due to the fact that ActiveRecord makes an implicit call to the database when calling comments
on the article model.
In Ecto, things are more explicit. You need to make a separate call to the database if you want associations:
article = Repo.get(Article, id)
article = Repo.preload(article, :comments)
IO.inspect(article.comments)
Or with the pipe operator:
article = Article |> Repo.get(id) |> Repo.preload(:comments)
IO.inspect(article.comments)
This is just something you will need to get used to. It seems annoying at first, but it makes N+1 query problems more visible and completely eliminates the need for libraries like Bullet.
This brings us to the next pitfall:
Trying to find "X for Elixir"
Over time, we get used to certain libraries and get to know them very well. Unfortunately, when switching to a new language, we will not be able to bring them with us.
Naturally, the first instinct is trying to replace them with something similar. I did that when starting out programming with Elixir. I am a big proponent of RSpec and also didn't want to miss the BDD syntax in Elixir.
It was not hard to find a similar library for Elixir: ESpec. In general, there is no problem with using a custom library to make you feel more comfortable. The difference though, is: RSpec is way more established in the Ruby world than ESpec is in the Elixir world.
Most projects just use the bundled ex_unit and it works pretty well, as José Valim pointed out himself.
Some libraries just work in a completely different way. A good example is Sidekiq. If you just want retries, you can achieve this with tasks. For a little bit more complex queuing, you can use a slim wrapper like Exq.
Applying the OOP mindset
This one is by far the biggest pitfall. If you have very little or no experience with functional programming, things might get tricky. Your mental model needs to shift. Don't worry, I will lay out some examples.
Model
Let's take the example from above, with preloading the comments from an article.
Let's change it a bit:
article = Repo.get(Article, id)
Repo.preload(article, :comments)
IO.inspect(article.comments)
Notice: we are not capturing the result of the preload
function.
The result is, as before:
#Ecto.Association.NotLoaded<association :comments is not loaded>
In Elixir, data structures, once initialized, will not change. You can apply changes to existing structures, but you will need to capture these changes. This immutability, by default, is something that I really learned to love, but nevertheless, it can be confusing at first.
Controller
Let's look at another example. A typical controller in Phoenix could look like this:
defmodule Blog.ArticleController do
use Blog.Web, :controller
def index(conn, params) do
{articles, kerosene} = Article.only_published |> Aepo.paginate(params)
render(conn, "index.html", articles: articles)
end
def show(conn, %{"id" => slug}) do
article = Repo.get_by!(Article, slug: slug)
render(conn, "show.html", article: article)
end
end
At first glance, it looks very similar to a typical Rails controller. If you take a second look, there is something odd happening here:
Look at the signature of the functions. Handling the state of the request in Phoenix is done utilizing this data structure. If you want to write the response, you have to apply changes to it, and in the end, return it.
View
Assuming you are building a web application that will also render the HTML, the nomenclature for views is a little different.
In Phoenix, there is a distinction between "templates" and "views." A Template is what you know from Rails already, snippets of HTML that include variables and function calls:
# web/templates/post/show.html.eex
<article>
<header>
<h3><%= @article.title %></h3>
<h4><%= @article.author %></h4>
<%= link "Back to Posts", to: post_path(@conn, :index) %>
</header>
<p><%= @article.body %></p>
<p><%= link "Edit", to: post_path(@conn, :edit, @article.id) %></p>
<section>
<h3>Comments</h3>
<%= link "New Comment", to: post_comment_path(@conn, :new, @article.id) %>
<%= for comment <- @article.comments do %>
<p><%= comment.author %></p>
<p><%= comment.body %></p>
<% end %>
</section>
</article>
When talking about views in Phoenix, you usually refer to "view modules," which is a module that provides helper functions for the template. Something in Rails is usually done in the view helpers:
defmodule Blog.PostView do
use PhoenixPlgrnd.Web, :view
def comment_count(article) do
length(article.comments)
end
end
This function can then be called from the template.
In Conclusion
I hope I was able to show you some common pitfalls when starting out with Phoenix. If you are aware of these pitfalls, chances are you will get pretty far with Phoenix and the learning curve will flatten.
The last piece of advice I'd like to give you is: even though Rails and Phoenix are quite different, your Rails knowledge will greatly benefit you when developing Elixir applications for the web. After all, you are developing against the same protocols.
Apply the knowledge as best you can and don't be afraid to reach out to people on StackOverflow or shoot me a message on Twitter.