loading...

Fun Things in Elixir

ryanflach profile image Ryan Flach ・5 min read

I've recently been learning Elixir, a functional programming language that leverages the Erlang VM. I've found a number of aspects of the language and its documentation to be both interesting and powerful. This post is a brief exploration into some of those elements.


1. Documentation syntax for function arity

If you're unfamiliar with arity, Wikipedia offers a good definition. In short, the arity of a function is the number of arguments that are expected or accepted by a function.

Functions are commonly listed in the Elixir docs in the form of name/arity. For example, the count functions in the Enum module would be listed as:

Enum.count/1
Enum.count/2

This indicates that there are two count functions: one which takes one argument, and a second that accepts two.

If a function takes no arguments, for example the new function in the Map module, it would be listed as:

Map.new/0

2. The Pipe Operator ( |> )

Elixir offers a beautiful shorthand for function chaining. Somewhat similar to promises in JavaScript, the pipe operator in Elixir automatically passes the value of the expression evaluation to the next function. If the function call on the right has an arity > 1, those arguments can still be provided, while the first argument is not necessary to provide explicitly, as it has been provided by the pipe operator.

Here are two identical operations — the first without the pipe operator, and the second refactored with use of the pipe operator.

# Without |>
my_string = "hello there, how are you?"
split_string = String.split(my_string, ",")
=> ["hello there", " how are you?"]

# With |>
split_string = "hello there, how are you?" |> String.split(",")
=> ["hello there", " how are you?"]

This example is very basic and chains only once. Even at this level, it's saved us a line of code without sacrificing readability. When chaining at several levels, the pipe operator really starts to shine. Look at this example from the official docs:

# Without |>
Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
=> 7500000000

# With |>
1..100_000
  |> Enum.map(&(&1 * 3)) 
  |> Enum.filter(odd?) 
  |> Enum.sum
=> 7500000000

3. Function definitions with clauses

Elixir allows you to create the same function with different clauses, utilizing the clause in its definition to determine which version of the function gets called.

defmodule SomeModule do

  def foo(arg \\ nil) when is_nil(arg) do
    IO.puts "No argument provided."
  end

  def foo(arg) do
    IO.puts "You provided the argument: #{arg}."
  end

end

In the above example, I've defined function foo/1 and set a default value for the arg of nil. If that function is called without an argument, a message is printed indicating such:

> SomeModule.foo
=> No argument provided.

Otherwise, the value of the arg is interpolated and printed:

> SomeModule.foo(12)
=> You provided the argument: 12.

4. In-code documentation and testing

Elixir allows documentation for public functions to be created by the developer within the code itself.

defmodule MainProgram.SomeModule do

  @doc """
  Takes an integer argument and returns the square of that value.
  """
  def squared(num) when is_integer(num) do
    num * num
  end

end

When running this module in IEx, Elixir's interactive shell, we'd have access to this documentation the same as any other, accessible with the h (help) command:

iex> h MainProgram.SomeModule.squared

                        def squared(num)

Takes an integer argument and returns the square of that value.

Even better, unit tests can be created within this same documentation:

defmodule MainProgram.SomeModule do

  @doc """
  Takes an integer argument and returns the square of that value.

  ## Examples
      iex> MainProgram.SomeModule.squared(2)
      4
      iex> MainProgram.SomeModule.squared(-4)
      16
  """
  def squared(num) when is_integer(num) do
    num * num
  end

end

When running mix test, the two tests listed under ## Examples will be run with all other tests in the test suite. To ensure this happens, the tests in the documentation need to be prefaced by iex> and be indented by four spaces, and in our SomeModuleTest file we need to list doctest:

defmodule MainProgram.SomeModuleTest do
  use ExUnit.Case, async: true
  doctest MainProgram.SomeModule
end

That's it. We now have tests that will run with mix test AND provide helpful examples as part of our documentation:

iex> h MainProgram.SomeModule.squared

                         def squared(num)

Takes an integer argument and returns the square of that value.

Examples
| iex> MainProgram.SomeModule.squared(2)
| 4
| iex> MainProgram.SomeModule.squared(-4)
| 16
iex>

5. Pattern Matching

Last but certainly not least is pattern matching, an extremely powerful aspect of Elixir. This topic could be a blog post of its own (and, in fact, does already exist as an excellent blog post), so this section will be a very brief introduction.

In Elixir, the = operator is officially the ‘match operator.' This is because it's actually performing a comparison of its left and right sides. Variable assignment still works as expected:

iex> x = 1
=> 1

But flipping sides can show us how comparisons are taking place:

iex> x
=> 1
iex> 1 = x
=> 1
iex> 2 = x
=> ** (MatchError) no match of right hand side value: 1

It's actually comparing the left and right sides to confirm a match. Note that, for variable assignment to occur, the variable must be on the left side of the match operator.

Pattern matching is more commonly used with tuples:

iex> {a, b, c} = {1, :hi, "3"}
{1, :hi, "3"}
iex> a
1
iex> b
:hi
iex> c
"3"

On the surface, this looks very similar to assigning multiple variables in Ruby:

2.3.1 :001 > a, b, c = 1, :hi, "3"
 => [1, :hi, "3"]
2.3.1 :002 > a
 => 1
2.3.1 :003 > b
 => :hi
2.3.1 :004 > c
 => "3"

But keep in mind that matching is still taking place, allowing us to only assign variables when certain conditions are met:

iex> {:ok, result} = {:ok, 13}
{:ok, 13}
iex> result
13

iex> {:ok, result} = {:error, :oops}
** (MatchError) no match of right hand side value: {:error, :oops}

As mentioned, pattern matching is worth diving into in more depth. In addition to the basic use case explored above, it can also be used with data structures and functions. If you're interested, check out the official site (where the final code example came from) or the aforementioned blog.

Posted on by:

ryanflach profile

Ryan Flach

@ryanflach

Software Developer and Returned Peace Corps Volunteer (Philippines 2010-2012). Writing Ruby, Rails, JavaScript, and constantly learning others in Denver, CO.

Discussion

markdown guide
 

Great introduction! Thanks. Was curious about the |> operator. Your description was just enough to scratch my itch :)