In Elixir, modules group functions together, much like a namespace. Usually, functions that reside in the same module are related to one another. You create a module using the defmodule construct:
iex> defmodule StringHelper do
...> def palindrome?(term) do
...> String.reverse(term) == term
...> end
...> end
{:module, StringHelper,
<<70, 79, 82, 49, 0, 0, 4, 0, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 119, 0,
0, 0, 11, 19, 69, 108, 105, 120, 105, 114, 46, 83, 116, 114, 105, 110, 103,
72, 101, 108, 112, 101, 114, 8, 95, 95, ...>>, {:palindrome?, 1}}
iex> StringHelper.palindrome?("abcd")
false
iex> StringHelper.palindrome?("abba")
true
In the preceding example we're also creating a function inside the StringHelper module, using the def construct, that checks whether a given string is a palindrome. This is a named function, and contrary to the anonymous functions, must be created inside a module.
Function names, like variable names, start with a lowercase letter, and if they contain more than one word, they are separated by underscore(s). They may end in ! and ?. The convention in the Elixir community is that function names ending in ! denote that the function may raise an error, whereas function names ending in ? indicate that that function either returns true or false–which is the case of our palindrome? function.
Note that unlike anonymous functions, we don't need to put a dot between the function name and the parenthesis when calling named functions. This is deliberate and serves to explicitly differentiate calls to anonymous and named functions.
As the implementation of our palindrome? function is very small, we can inline it with the following syntax:
def palindrome?(term), do: String.reverse(term) == term
This works with other constructs that use the do ... end syntax, such as defmodule or if. We will explore if (and other classical control flow mechanisms) in the Control-flow section.
Before we go any further, as our examples are getting bigger, we must discuss how you can write Elixir code in files. As you can see in the previous example, you can define modules on an IEx session–however, any typo while writing them results in having to start from the beginning.
Put the contents of the last example in a file–let's call it "string_helper.ex" (we usually name the file with the name of the module we're defining in it). Elixir source code files may have two extensions: .ex or .exs. The difference between them is that the former is compiled to disk (creating .beam files), while the latter is compiled only in memory. We mostly use the .ex extension when working on a real application, except for the test files that use the .exs extension (as there's no point in compiling them to disk).
Having your file created, you can use the elixirc command in your terminal to compile it, passing the name of the file whose contents you want compiled. More interestingly, you can pass the filename to the iex command (iex string_helper.ex in our case). This will make Elixir compile your file, which will make our StringHelper module (and its functions) available in the IEx session. If you're already inside the IEx session and want to compile a new file, you can use the c command, passing the filename as a string:
iex> c("examples/string_helper.ex")
[StringHelper]
You can also nest modules:
$ cat examples/nesting_modules.ex
defmodule Helpers do
defmodule StringHelper do
# StringHelper code goes here
end
end
In the preceding example, the line starting with # is commented. That's the syntax to comment lines in Elixir. There's no syntax for multi-line comments–if you want to comment a block of code, prepend each line of that block with #.
However, during compilation, Elixir will prepend the outer module name to the inner module name, and separate them with a dot. This is just an amenity, as there is no relationship between these two modules. This syntax is equivalent to the following one, which is used much more in Elixir applications:
$ cat examples/nesting_modules_inline.ex
defmodule Helpers.StringHelper do
# StringHelper code goes here
end
We'll now explain the concept of arity, with our palindrome? function as an example. Named functions in Elixir are identified by their module name, the function's own name, and their arity. The arity of a function is the number of arguments it receives. Taking this into account, our palindrome? function is identified as Helpers.StringHelper.palindrome?/1, where /1 represents the arity of the function. You'll be seeing this notation a lot when browsing through Elixir documentation.
This concept is important because functions with the same name but different arities are, in effect, two different functions. However, for a human, it wouldn't make much sense that two functions with the same name (but different arities) are unrelated. As such, only use the same name for different functions when they are related to one another.
The common pattern in Elixir is to have lower-arity functions being implemented as calls to functions of the same name but with a higher arity. Let's extend our module with an emphasize function:
$ cat examples/string_helper_emphasize.ex
defmodule StringHelper do
def palindrome?(term) do
String.reverse(term) == term
end
def emphasize(phrase) do
emphasize(phrase, 3)
end
def emphasize(phrase, number_of_marks) do
upcased_phrase = String.upcase(phrase)
exclamation_marks = String.duplicate("!", number_of_marks)
"#{upcased_phrase}#{exclamation_marks}"
end
end
Here, we can observe it in action:
iex> StringHelper.emphasize("wow")
"WOW!!!"
iex> StringHelper.emphasize("wow", 1)
"WOW!"
We've used the def construct to create functions. By using it, our functions are exported and can be called in other modules. If you want to change this behavior, and make a function only available within the module where it's defined, use the defp construct.
The function with an arity of 1 is implemented by simply calling emphasize/2. This is useful when you want to offer a broad interface on your module, which allows you to have some clients that simply want to call emphasize/1 and not have to specify the number of exclamation marks, but also have some other clients that want to call emphasize/2 and specify the number of exclamation marks.
When the code is as simple as in this example, this multitude of functions is not necessary, as you can achieve the same end result using default arguments. We do that by using the \\ operator in front of the argument name, and then the default value it should have:
$ cat examples/string_helper_emphasize_with_default_args.ex
def emphasize(phrase, number_of_marks \\ 3) do
upcased_phrase = String.upcase(phrase)
exclamation_marks = String.duplicate("!", number_of_marks)
"#{upcased_phrase}#{exclamation_marks}"
end
This will generate two functions with the same name and different arities, as in the last snippet. If your function has multiple bodies, as in the next example, you must define a function header with the default arguments defined there:
$ cat examples/string_helper_emphasize_with_function_header.ex
def emphasize(phrase, number_of_marks \\ 3)
def emphasize(_phrase, 0) do
"This isn't the module you're looking for"
end
def emphasize(phrase, number_of_marks) do
upcased_phrase = String.upcase(phrase)
exclamation_marks = String.duplicate("!", number_of_marks)
"#{upcased_phrase}#{exclamation_marks}"
end
In this example, we're also seeing an example of how we can use pattern matching in named functions. Note that the order in which we define our functions matters. Elixir will search from top to bottom for a clause that matches. If we had put the clause where we're matching against 0 on the second argument at the end, that definition of the emphasize function would become unreachable, as the other definition is more general and always matches. Elixir will help you avoid these situations, as it will emit a warning during compilation, alerting you of this situation.
Apart from using pattern matching (as we've seen in this example and on anonymous functions), on named functions we can use guard clauses, which extend on the pattern matching mechanism and allow us to set broader expectations on our functions. To use a guard clause on a function, we use the when clause after the list of arguments.
To see an example of this, we will use a guard clause on our palindrome? function. Up to this point, we were accepting an argument of any type. If we passed an integer to this function, an error would be raised, as we would be trying to call String.reverse on an integer. Let's change that:
$ cat examples/string_helper_palindrome_with_guard_clause.ex
def palindrome?(term) when is_bitstring(term) do
String.reverse(term) == term
end
def palindrome?(_term), do: {:error, :unsupported_type}
We now state that we're expecting bitstring as an argument. We've also created a new definition of our function, which runs when the match doesn't occur on the first definition. Here it is in action:
iex> StringHelper.palindrome?("abba")
true
iex> StringHelper.palindrome?(123)
{:error, :unsupported_type}
Using guard clauses in our functions can lead to a lot of duplication, since we may be repeating the same clause over and over again. To combat this, Elixir 1.6 introduced the defguard construct, which allows us to define clauses that can be reused.
Moreover, using this construct may improve the readability of your code, since we can extract complex guard clauses and give them descriptive names. Let's see the previous example implemented using defguard:
$ cat examples/string_helper_palindrome_with_defguard.ex
defguard is_string(term) when is_bitstring(term)
def palindrome?(term) when is_string(term) do
String.reverse(term) == term
end
def palindrome?(_term), do: {:error, :unsupported_type}
In this simple example, there's no clear advantage to using this construct. However, as your modules, along with your guard clauses, grow more complex, this technique becomes incredibly useful. Note that you can use defguardp to define a guard clause that is not exported, and can only be used within the module where it's defined.
You can use other type-checking functions in guard clauses, as well as comparison operators, and also some other functions. You can find the full list at
https://hexdocs.pm/elixir/guards.html.
To end this section, we will now showcase one of the most eminent features of the language: the pipe (|>) operator. This operator allows you to chain function calls, making the flow of your functions easy to read and comprehend. This operator takes the term that's at its left, and injects it as the first argument on the function at its right. This seemingly insipid feature increases the readability of your code, which is amazing since code is read many more times than it is written. To see this operator in action, let's add some more logic to our palindrome? function: We will now remove leading or trailing whitespaces from the term we're checking, and we'll also make our comparisons case-insensitive. This is the result:
$ cat examples/string_helper_palindrome_with_pipe_operator.ex
def palindrome?(term) do
formatted_term = term
|> String.trim()
|> String.downcase()
formatted_term |> String.reverse() == formatted_term
end
While the impact may seem negligible in this simple example, you'll see the expressiveness this operator brings as we build our application throughout the book.