Is `end` keyword optional in elixir when using `def`

defmodule LousyCalculator do
  @spec add(number, number) :: {number, String.t}
  def add(x, y), do: {x + y, "You need a calculator to do that?!"}

  @spec multiply(number, number) :: {number, String.t}
  def multiply(x, y), do: {x * y, "Jeez, come on!"}
end

What i can see is the def doesnt have end there. If it is optional, is it a good practice? Thanks

No, the end is not optional! When you use do, you have to use end, you can use one-line-syntax though, then you have to replace do foo() end with , do: foo. Be aware of the comma and the colon!

Also please try to avoid putting screenshots your posts, when copy-pasting the code and linking to the source is also possible. Some users knowing an answer to your question might be unable to view pictures in general or temporarily (I’m often on a mobile connection and strip many images to save up bandwith).

2 Likes

Thanks for your question @muhajirframe. def and defp have two forms:

Normal:

  def my_func(args) do
    "result"
  end

Inline (note the , and :):

  def my_func(args), do: "result"

When defining multiple function heads, the inline form can make it easier to visually read what arguments are being pattern matched. Typically the inline form should be used when the function body is short.

  def my_func(:ok), do: ...
  def my_func(:error), do: ...

Either way, both forms are valid and you can use them interchangeably.

1 Like

This behaviour is not related to def and defp in any way. It does apply anywhere where you can pass in a do block. defmodule, defmacro, if, or even your own macros and functions. A very similar thing does work for else as well.

3 Likes

Absolutely correct. The original question was specifically asking about def and defp so I just wanted to make things as clear as possible.

2 Likes

Just to elaborate further on what’s already been mentioned:

A do/end block is really just a keyword list under the hood, with a do key and some expression as the value. You can read a little more about it here. In other words

do
  x + y
end

can also be represented as

[do: x + y]

def and defp are just macros, so although they look a lot like keywords they are really just calls and therefore have the same characteristics as any other function/macro call with regards to syntax. They have an arity of two: the first argument is the call and the second is the expression. Another way to write a function definition could be:

def(add(x, y), [do: x + y])

When written this way it’s no longer ‘disguised’ as a keyword. It’s clear that it is just a macro call. Since parenthesis are optional for calls we can also use the form:

def add(x, y), [do: x + y]

And since the brackets on keyword lists are optional when used as the last parameter in a function call, we can also use this form:

def add(x, y), do: x + y

And now we’ve arrived at the ‘single-line’ form. Its clearer now why the comma and colon come into play. It isn’t a special syntax used for single line function definitions: the comma is just the parameter separator used in all function calls and the colon is for the key in the keyword list.

Finally, we can represent the do keyword list using the do/end block syntax and get:

def add(x, y) do
  x + y
end

When using a do/end block as the the final argument in a function we can omit the leading comma (I’m not sure if I’ve ever seen this formally stated before, but it does seem to behave that way).

This same behavior applies for all other macros. For example, if is also a macro, so you can break it down the same way:

if(something(), [do: foo()])

if something(), [do: foo()]

if something(), do: foo()

if something() do 
  foo()
end

An else block (as well as try, catch, rescue, and a few others) is really just another key on the do keyword list, so in other words you can introduce the else block the same way:

if(something(), [do: this(), else: that()])

if something(), [do: this(), else: that()]

if something(), do: this(), else: that()

if something() do
  this() 
else
  that()
end

To summarize: def and defp are just macros calls, and as such follow the same syntax rules as other calls. Technically there aren’t two separate forms for function definitions, there are just varying degrees of sugar you can use to dress up function calls. That being said, you will encounter these two ‘forms’ enough in practice that it makes sense to distinguish them, so while there aren’t technically two forms I think most people will recognize them as being the two de-facto forms.

The single-line variant is used often, here are some sections from an elixir style-guide to give you an idea of when to use them vs the block variant: https://github.com/christopheradams/elixir_style_guide#single-line-defs

Edit: There is also another bit of sugar that I left off in these examples. Keyword lists are really lists of 2-value tuples in the form {atom key, mixed value}. So for example:

[do: foo(), else: bar()]

is really

[{:do, foo()}, {:else, bar()}]

Which makes the if example become in full:

if(something(), [{:do, this()}, {:else, that()}])
if(something(), [do: this(), else: that()])
if something(), [do: this(), else: that()]
if something(), do: this(), else: that()
if something() do
  this() 
else
  that()
end

It can be a little bit overwhelming and puzzling at first having to deal with these little syntactic intricacies. My first reaction to seeing the single line def form was “why is that comma there?! and why a colon too?!”. I thought having all these different layers of optional syntax was bizarre, and personally I would prefer there to be less ways to write something, not more.

I was initially turned off by it until I realized that most of what I considered to be language keywords were actually just macros. Then it started to make sense why some of these syntax choices were made (at least why I think they were made): they give you the ability to make macros that look identical to traditional keywords. This is why you can’t tell that def is really just a macro until you squint and peel back the layers.

Having this capability allows you to expand the language to add expressive, domain specific constructs. Phoenix has several great examples of this, like defining plug pipelines, route scopes, things like attaching plugs directly to a controller, etc. Ecto’s query DSL is another awesome example, same with ExUnit’s assertion and test definitions. Some of these constructs feel so natural and expressive that they feel like they are a part of the language itself, but they are actually more like libraries.

Anyways, I think it is awesome. I may have gotten a little bit carried a way here, but hopefully I’ve helped in some way.

22 Likes

https://hexdocs.pm/elixir/master/syntax-reference.html

So, the end IS optional?

No, it is not optional. There are 2 ways of writing body of function - as do-block (and then you need to use end) and as keyword list do: foo. do-block is a sugar for another one, but these are separate constructs.

4 Likes

It isn’t optional, but there’s a special syntax in which its not required.
i.e. def foo(), do: --some code here–

This made me think what tokens are optional in Elixir’ s syntax, and the only thing that I could come up with were:

  • trailing coma at the end of the elements in a list, map, tuple.
  • return values which default to nil. (not a toke though)

Anybody can think of anything else?

It is other way around :wink:

But I guess, I’m right?

I think he means the “special syntax” is the do ... end block because under the hood it’s a keyword list as @kylethebaker outlined. So do THIS end is the special syntax for [{:do, THIS}].

2 Likes

As @stevensonmt said - technically do CODE end is the special syntax, as you can easily remove it from language and it will only affect the readability, but will not impose any limitations. do-blocks are added on top of keyword lists, not other way around.