Today I'll cover three basic topics:
-
Mistakes and misuses every Erlanger does when he starts to write in Elixir
-
Dependencies and Mix
-
Writing OTP-enabled Elixir applications
As I stated in Issue #1: I'm focusing on things that in my opinion can help new Elixir developer before he or she starts her first Elixir production project.
We, erlangers, do love our case
expressions. We use those here and there and
(what seems strange to Erlang newcomers) we almost always use those instead of
somehow clumsy if
expression.
In Elixir -- on the other hand -- if
is really useful. As a rule of thumb,
when you want to check if a boolean value is true, you should use if
, not
case
.
But wait, there's more!
Elixir introduces concept of truthy and falsy values.
Everything is truthy but :false
(which is equal to false
, observe:
iex(1)> false == :false #=> true
) and nil
. The later is a very special
value in Elixir. It is used by its constructs in implicit returns, observe:
iex(2)> :nil == nil #nil is atom :nil
true
iex(4)> f = fn() -> end; f.() #function with no body implicitly returns nil
nil
iex(6)> if false, do: :nevermore #nil is returned implicitly from an if with no else
nil
Let's now compare how if
behaves in Erlang and Elixir:
2> if ok -> shmoo; true -> con end.
con
iex(7)> if :ok, do: :shmoo, else: :con
:shmoo
As you see, as in Elixir we treat atoms as truthy values, :shmoo
is returned
while in Erlang atom ok
is not a boolean, it matches fallback branch and
returns con
.
Hopefully that will remove if
-anxiety and give you understanding of how to
use this conditional operator in your code.
Now let's talk about case
in Elixir. One thing to remember is that Elixir
allows multiple variable assignment, observe:
iex(8)> a = :ok
:ok
iex(9)> a = :notok
:notok
Thus, by default case assigns value to the match expression and when you face that for the first time you can find yourself in a tough spot:
iex(1)> a = :should_be_this
:should_be_this
iex(2)> b = a
:should_be_this
iex(3)> case b do
...(3)> a -> :expected
...(3)> :should_be_this -> :unexpected
...(3)> end
:expected
iex(4)> a
:should_be_this
iex(5)> b
:should_be_this
iex(6)> b = :shouldnt_be_this
:shouldnt_be_this
iex(7)> case b do
...(7)> a -> :unexpected
...(7)> :shouldnt_be_this -> :expected
...(7)> end
:unexpected
iex(8)> a
:shouldnt_be_this
iex(9)> b
:shouldnt_be_this
iex(10)> a == :should_be_this
false
So the bottom line: when you want to pattern-match, don't forget the cap ^
:
iex(11)> a = :should_be_this
:should_be_this
iex(12)> b = :not_a
:not_a
iex(13)> case b do
...(13)> ^a -> "#{a} matched #{b}"
...(13)> _ -> "#{a} didn't match #{b}"
...(13)> end
"should_be_this didn't match not_a"
Also -- the mistake I always make -- writing case ... of
instead of
case ... do
can produce not-fun-at-all debugging session, so once you get
unexpected end
error that's close to the end of module definition or to the
end of file, look up your sources for case ... of ... end
typos.
Okay, so, this one is a really short hint. As for today, Mix can't fetch rebar deps, so you should put them into your mix.exs explicitly.
Indent those to show that those are deps like Mr. Yurii Rashkovskii does:
defp deps do
[
{:validatex, github: "yrashk/validatex"},
{:hackney, github: "benoitc/hackney"},
{:edown, github: "esl/edown"},
{:genx, github: "yrashk/genx"},
{:cowboy, github: "extend/cowboy"},
{:ranch, github: "extend/ranch"},
{:mimetypes, github: "spawngrid/mimetypes"},
{:lagerex, github: "yrashk/lagerex"},
{:exreloader, github: "yrashk/exreloader"},
{:erlpass, github: "ferd/erlpass", compile: "rebar compile deps_dir=.."},
{:proper, github: "manopapad/proper"},
{:bcrypt, github: "spawngrid/erlang-bcrypt"},
{:relex, github: "yrashk/relex"},
{:exconfig, github: "yrashk/exconfig"},
]
end
Or do as I do to remove the boilerplate of manually typing in required applications from the dependency list in the applications list (full mix.exs):
defmodule Recipex.Mixfile do
use Mix.Project
def project do
[ app: :recipex,
version: "0.0.1",
deps: deps ]
end
# Configuration for the OTP application
def application do
[
applications: List.foldl(deps!, [], function do
{{app, _source}, :req}, acc0 -> [app|acc0] # if dep is marked with :req, start it.
_, acc0 -> acc0 # else, skip it in the applications list
end) ++ [:mix] ++ env_apps(Mix.env), # start otp/elixir apps and env-apps
mod: {Recipex.App, []}
]
end
defp env_apps(:dev), do: [:exreloader]
defp env_apps(_), do: []
# Returns the list of dependencies in the format:
# { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" }
defp deps do
#Enum.map ensures that compilation order is preserved.
Enum.map deps!, fn(x) -> :erlang.element(1, x) end
end
defp deps! do
[
{{:genx, github: "yrashk/genx"}, :req},
{{:xup, github: "yrashk/xup"}, :req},
{{:mysql, github: "manpages/erlang-mysql-driver"}, :req},
{{:proper, github: "manopapad/proper"}, :nreq},
{{:cowboy, github: "extend/cowboy"}, :req},
{{:ranch, github: "extend/ranch"}, :req},
{{:lambdatools, github: "manpages/lambdatools"}, :req},
{{:erlgit, github: "gleber/erlgit"}, :req},
{{:erlsemver, github: "gleber/erlsemver"}, :nreq},
{{:sh, github: "gleber/sh"}, :nreq},
{{:erlydtl, github: "evanmiller/erlydtl"}, :nreq},
{{:exlager, github: "khia/exlager"}, :req},
{{:jsx, github: "talentdeficit/jsx"}, :req},
{{:ossp_uuid, github: "yrashk/erlang-ossp-uuid"}, :req},
{{:exconfig, github: "yrashk/exconfig"}, :req},
{{:exreloader, github: "yrashk/exreloader"}, :nreq},
]
end
end
I'll make that mix.exs slightly more beautiful and will update that guide right
after. My thought is that we should make the second element of deps!
tuple
be :req
or any atom that could be returned from Mix.env
, or nil
.
In such way we'll be able to declare function deps_apps!
that will start all
the :req
dependencies and all the dependencies that match against current
output of Mix.env
.
Anyway, bottom line is that you put deps of your deps into your deps.
During the first couple of days of Elixir I was still getting used to syntax (never wrote in Ruby before, twice in Python: for Convergence and ICFPC), I was trying to master calls to the standard OTP functions from Elixir modules. It was rather hard not to mess something up, so in frustration I asked Mr. Rashkovskii to peek at my OTP code and tell what am I doing wrong. He answered that question but offered to use GenX instead. As I was both sceptical and anxious about whole Elixir, I was too lazy and too scared to use GenX off the bat (which was a bad-bad mistake).
Eventually I started to use GenX to remove boilerplate code when defining gen_servers and supervisors, but then Mr. Rashkovskii released Xup that replaced and improved GenX supervisor macros.
I'll just let the code speak, okay?
import Xup
defsupervisor Future.Sup, strategy: :rest_for_one do
worker do: [id: Future.Mon]
supervisor SOFO, strategy: :simple_one_for_one do
worker do: [id: Future.Srv, shutdown: :brutal_kill]
end
end
defmodule MockingBird do
use GenServer.Behaviour
import GenX.GenServer
defrecord State, id: nil
defcall get_state, from: _from, state: state, do: {:reply, state, state}
def start_link(id) do
:gen_server.start_link(__MODULE__, id, [])
end
def init(id), do: {:ok, State.new(id: id)}
end
Bottom line: don't try to avoid using GenX and Xup in your daily programming routine, those are truly awesome macro-libraries that make your OTP code crystal clear and process of writing down OTP trees joyful and intuitive.