At honestbee, most of the projects are done in Rails and so majority of us here are Rubyist. When I first started working with Elixir, almost all of initial Google searches where like “Rails X equivalent in Elixir”. To my surprise, there are a lot of people like me :).
Phoenix
We use Phoenix Framework for developing REST APIs and web applications with Elixir.
Ecto
Ecto is to Phoenix what Active Record is to Rails. I was very impressed by its Query API and syntax. If you have worked with .NET LINQ you would find Ecto very similar.
As I started developing my database models, very quickly I started missing enums
. In Rails, we can easily do something like
class User
enum status: {active: 1, inactive: 2}
end
and then I can use the value for user statuses with
User.statuses[:active] # this will give 1
User.statuses[:inactive] # this will give 2
Sadly, Ecto does not have anything similar to enum. I find it very useful to abstract the id values with meaningful names as it allows me to change the values later without affecting my code implementation.
Macros
In Elixir, you can define a macro which will expand the code in place where you use them.
Before, we take a look at how to write a macro, we need to understand quote
and unquote
Quoting
Everything in Elixir is represented as a tuple with three elements. For example, the following function print("hello world")
is represented as
{:print, [], ["hello world"]}
First element is the function name, second is the keyword list containing metadata and third contains the arguments.
You can get this representation by doing (in your iex
console)
iex> quote do: print("hello world")
{:print, [], ["hello world"]}
To get your string representation back from a quoted expression, you can do
iex> Macro.to_string({:print, [], ["hello world"]})
"print(\"hello world\")"
Unquoting
Sometimes, you may want to use a variable value in your quoted expression, for example,
iex> user = "Taher"
iex> quote do: print("hello " <> user)
{:print, [], [{:<>, [context: Elixir, import: Kernel], ["hello ", {:user, [], Elixir}]}]}
Notice how we got {:user, [], Elixir}
instead of the value of that variable. This is where unquote
is useful.
iex> user = "Taher"
iex> quote do: print("hello " <> unquote(user))
{:print, [], [{:<>, [context: Elixir, import: Kernel], ["hello ", "Taher"]}]}
Now we have the value in the quoted expression which is what we want.
Enum macro
To create an enum
in Elixir, I created a macro, that would take the name of the enum and a block which will contain the values (a map)
defmacro enum(name, [do: block]) do
end
The above macro can be used as
enum "status" do
%{
active: 1,
inactive: 2
}
end
Now, I need a way to read the enum values i.e. be able to do something like
MyModel.status[:active] # this should give me 1
To achieve this, we can create a method with the enum name which returns the map. Since our method will be in the macro, when we do import MyEnumMacro
in the MyModel
module, it will expand in place and thus will be available as a method in the module.
A macro needs to be a quoted expression so that it can be expanded by the compiler when we do import. So lets do some quoting-unquoting, shall we?
defmacro enum(name, [do: block]) do
enum_values = <Fetch values from block. More on this later>
quote do
def unquote(:"{name}")() do # unquoting because we want to use the actual value of "name"
unquote(enum_values)
end
end
end
What we did here is, created a quoted expression which contains a function with enum name and it returns the enum value map.
Lets get the enum values from the do block
enum_values =
case block do
{_, _, values} when is_list(values) ->
values
_ ->
quote do
{:error, "Only maps allowed as values of enum"}
end
end
If the case statement above looks alien, then read up a bit about case and pattern matching in Elixir.
Remember that everything in Elixir is represented as a three element tuple? Our do block is passed to our macro function as a tuple with the third element as our map in a keyword list (the check in the when clause). So if the format is correct, we pattern match and fetch the value map which is used as a return value of the function. If we pass something else other than a map, the pattern matching fails and the enum value is set with an error tuple. So when you try to fetch the enum value, you will get an error.
Here is the complete code
defmodule EnumsHelper do
@moduledoc false
defmacro enum(name, [do: block]) do
enum_values = case block do
{_, _, values} when is_list(values) ->
values
_ ->
quote do
{:error, "please provide Map with %{key: value} for enum"}
end
end
quote do
def unquote(:"#{name}")() do
unquote(enum_values)
end
end
end
end
In your model module, you can define your enum by importing the helper
defmodule User do
@moduledoc false
import EnumsHelper
enum "status" do
%{
active: 1,
inactive: 2
}
end
end
You can then use your status enum like,
my_user = User |> Repo.get(1)
if my_user.status == User.status[:active] do
# do stuff
end
Summary
I hope you find this useful if you are working with Ecto and Elixir. I find Elixir very refreshing and exciting. If you are working with Ruby and are interested in making the switch, read up some discussions about progression from Ruby to Elixir here - quora and here - reddit.