Exploring with, the Elixir special form

2016-05-27 by Marcos Almonacid

This is my short exploration of with , the new Elixir special form introduced in v1.2 .

Definition

If we check the documentation, it says that with is used to combine matching clauses. And if all the clauses match, the do block is executed, returning its result. Otherwise the chain is aborted and a non-matched value is returned:

opts = %{ width: 10 , height: 15 } with { :ok , width} < - Map .fetch(opts, :width ), { :ok , height} < - Map .fetch(opts, :height ), do: { :ok , width * height} #=> {:ok, 150} opts = %{ width: 10 } with { :ok , width} < - Map .fetch(opts, :width ), { :ok , height} < - Map .fetch(opts, :height ), do: { :ok , width * height} #=> :error

The documentation also says: variables bound inside with won’t leak, and also it allows “bare expressions”:

width = nil opts = %{ width: 10 , height: 15 } with { :ok , width} < - Map .fetch(opts, :width ), double_width = width * 2 , { :ok , height} < - Map .fetch(opts, :height ), do: { :ok , double_width * height} #=> {:ok, 300} width #=> nil

Some usages

This looks cool. But when should we use it? There are some situations where using with is a good idea. I’m going to mention 2 of them.

Replacing nested case statements

Let’s say we have a function to setup a socket that listens on a specific port, accept a connection on this socket, and wait for incoming packets. We could have something like:

def server (port, opts) do case :gen_tcp .listen(port, opts) do { :ok , lsock} -> case :gen_tcp .accept(lsock) do { :ok , sock} -> do_recv(sock) error -> error end error -> error end end def do_recv (sock) do case :gen_tcp .recv(sock, 0 ) do { :ok , packet} -> handle_packet(packet) do_recv(sock) { :error , :closed } -> :ok end end

server/2 has 2 nested case’s. We can rewrite it using with :

def server (port, opts) do with { :ok , lsock} < - :gen_tcp .listen(port, opts), { :ok , sock} < - :gen_tcp .accept(lsock), do: do_recv(sock) end

This new implementation is shorter and more readable.

Validations

with can also be used to validate data before doing something with this data.

Let’s say we want to create a new user in our database by storing it in our database; but in order to do so, we have to run some validations over the data.

If we use with , we could write something like:

def create (user) do with :ok < - validate_name(user), :ok < - validate_email(user), :ok < - validate_token(user), :ok < - validate_location(user), do: persist(user) end

The code looks pretty simple.

We run every validation before calling persist/1 . If one of the validations returns something different than :ok , for example, validate_email/1 returns { :error, :invalid_email } , the chain will be aborted and create/1 will return the error tuple.

(We can implement create/1 in a few different ways. Using with is just one more)

What about the ‘Let it crash’ culture?

Ok, with is a nice special form, our code looks simple and readable when we use it. But I’m an Erlang developer, so if something doesn’t return the result that I’m expecting, the process running that code must die and its supervisor might restart it (depending on the restart strategy).

We could say that with hides MatchError crashes. So that, using with is kind of using try/catch statements. Well, I think that’s not really true. Because whatever with returns is going to be an expected result, regardless if it’s a successful response or an error response. For instance, if we call our create/1 function and it returns { :error, reason } , we might want to do something with that error (log it, send a notification, etc) before finishing the process.

case create(user) do { :ok , id} -> { :ok , id} { :error , reason} -> handle_creation_error(user, reason) end

Conclusion

This new special form is a good tool to write simple code when it’s used in the right situation. As any other tool, trying to use it everywhere would be a mistake.

It doesn’t hide MatchError crashes, it simply has different possible returns. So it’s not breaking the 'Let it crash’ rule. For example, we know that our create/1 function returns { :ok, id } or { :error, reason } , if we only want to accept { :ok, id } we simply match the result to it:

{ :ok , id} = create(user)

And as a downside, I would say that I’m starting to get a little “scared” about seeing new special forms, because they provide new ways to do something that we are already doing. And having multiple ways to do the same thing makes me remember Ruby and its “infinite” alternatives to write the same logic, which sometimes makes me feel that I’m not able to choose the right one. Anyway, Elixir doesn’t have that problem (yet). So, all in all, I can say I’m enjoying this new special form.