Introducing Modules and Functions

A quick overview and introduction to an Elixir module and function is needed.

In functional programming, it is just “functions” and “data”. Functions are the instructions of how to transform some given data. Modules are containers for functions that also provide a namespace.

Modules

Modules serve both as a container for functions and as a namespace.

A module is defined using the defmodule command and the name starts with an uppercase letter.

defmodule MyFoo do

  def foo do
    "Hello!"
  end

end

In order to execute the foo function, I need to provide the namespace to the function. You can copy/paste the above module code into IEx to play with it.

MyFoo.foo
#=> "Hello!"

Parenthesis are optional in Elixir. The above could also be written as MyFoo.foo(). If there is ambiguity about the syntax then the parenthesis are required by compiler.

Functions in IEx

Functions must be defined inside of a module. This makes playing with them less convenient in IEx. Typing and editing module code into IEx is awkward. Here are three ways you can play with modules at this stage:

  1. Edit the code in a text file and paste it into IEx
  2. Edit the code in a text file and execute the file as a script
  3. Start a “mix” project

The first one you can do easily enough on your own. Editing the code and pasting it into IEx will “re-write” the module.

Running an Elixir Script

The second option is to create an Elixir script file. You can execute the script from the command line. This makes it easier to work with slightly more complex code samples.

Thinking Tip

Read “Running an Elixir File as a Script” for a walk-through of how to do it. It includes explanations of what’s happening and how to recognize when you’ve grown out of a script.

Creating a Simple Mix Project

The third option is to create a simple Elixir “mix” project. I recommend this approach for the following reasons:

  • Simple to create
  • A mix project is very small
  • Easily supports organizing your code into multiple files
  • Starts you off with a testing framework setup
Thinking Tip

Read “Creating Your First Mix Project” for a walk-through of how to do it. It includes tips on working with your code in a mix project.

For more details on how to do this, please read the post “Creating Your First Mix Project” which explains it in more detail.

Function “Arity”

In Elixir we talk about functions and their “arity“. Meaning the number of arguments that a function takes. 

Let’s add two functions called greeting to our MyFoo module. You can copy/paste this into IEx.

defmodule MyFoo do

  def foo do
    "Hello!"
  end

  def greeting(name) do
    "Hello #{name}!"
  end

  def greeting(name, extra_greeting) do
    "Greetings #{name}! #{extra_greeting}"
  end

end

In IEx, I can use auto-completion to see the functions available on the module I just defined. After MyFoo. press the TAB key to see the auto-complete options.

iex> MyFoo.
foo/0         greeting/1    greeting/2    
iex> MyFoo.

It shows the two different greeting functions. One with the /1 and the other with a /2 to identify how many arguments they take.

Function Return Values

Elixir uses an “implicit return” for functions. You don’t explicitly say “return” this value. Elixir returns the value of the last expression as the function result.

In the “MyFoo.foo” example, the return is the string "Hello!" because it was the last value expressed in the function. There is no way to not return a value. Given that this is Functional Programming, every function returns a value! You may choose to ignore it, but it will return something.

In this example, let’s create a function that does nothing and returns nothing. What happens when we call it?

defmodule MyNewFoo do

  def do_nothing do

  end

end

MyNewFoo.do_nothing
#=> nil

It has to return something so it returns nil.

No Early Return?

Given that a function always returns something and the last thing the function does is used as the return value, some people ask, “Why can’t I return explicitly at an earlier point? Why doesn’t Elixir have an explicit return?”

The short answer is, “Erlang doesn’t have explicit returns either and Elixir is built on Erlang.”

The longer answer is, with pattern matching, you have a new tool to solve old problems in a new way. Once you get comfortable with pattern matching in functions, you won’t miss early returns. Your Elixir code becomes more understandable and readable without them. If this is a concern of yours, just know for now that it’ll be okay. Promise.

What if I don’t want to return anything?

There are times when a function does some work or creates a side-effect and there is nothing meaningful to return. A common pattern you’ll see in Elixir and Erlang is that those functions return the atom :ok.

defmodule Testing do

  def do_stuff do
    # do stuff that can't fail or any errors are handled
    :ok
  end

end

Testing.do_stuff
#=> :ok

Private Functions

Functions defined with the defp macro are “private”. They are not exported from the module. The can be called from within a module, but are not available outside the module.

defmodule MyApp do

  def public_do_work(input) do
    private_work(input)
  end

  defp private_work(_input) do
    IO.puts "working!"
  end
end

MyApp.public_do_work(123)
#=> working!
#=> :ok

MyApp.private_work(123)
#=> ** (UndefinedFunctionError) function MyApp.private_work/1 is undefined or private
#=>     MyApp.private_work(123)

Passing a Function by Name

We can refer to a specific function by name using it’s arity. We use the & to express that we want a reference to the function.

say_hello = &MyFoo.greeting/1
#=> &MyFoo.greeting/1

I can now execute the function bound to the variable using a . to identify that the variable references a function to execute and isn’t the name of the function itself.

say_hello.("Mark")
#=> "Hello Mark!"

Using this technique, we can pass a function as a parameter into other functions.

defmodule MyFoo do

  def greeting(name) do
    "Hello #{name}!"
  end

  def process_name(name, fun) do
    fun.(name)
  end

end

Now we can let the process_name/1 function combine a piece of data and a function that we provide.

MyFoo.process_name("Mark", &MyFoo.greeting/1)
#=> "Hello Mark!"

MyFoo.process_name("Mark", &IO.puts/1)       
#=> Mark
#=> :ok

MyFoo.process_name("Mark", &String.to_atom/1)       
#=> :Mark

Default Arguments

An argument to a function can be given a default value using the \\ operator. It looks like this:

defmodule MyFoo do

  def some_function(value \\ :default) do
    value
  end

end

Let’s create a new greeting function that also gives a compliment. We’ll provide a default compliment.

defmodule MyFoo do

  def greeting_with_compliment(name, compliment \\ "You look nice today!") do
    "Greetings #{name}! #{compliment}"
  end

end

When I execute the function with only a name given, the default compliment value is used. I can provide an override compliment to use for that specific case.

MyFoo.greeting_with_compliment("Tom")
#=> "Greetings Tom! You look nice today!"

MyFoo.greeting_with_compliment("Bill", "That color suits you.")
#=> "Greetings Bill! That color suits you."

Multiple Functions are Created

When you give a default value to an argument, Elixir creates 2 versions of the function.

Using auto-completion, I can see that two functions were created.

iex> MyFoo.greeting_with_compliment
greeting_with_compliment/1    greeting_with_compliment/2    

We didn’t explicitly create a greeting_with_compliment/1 function. That one was created for us. If we could see the generated code, it would essentially look like this.

def greeting_with_compliment(name) do
  greeting_with_compliment(name, "You look nice today!")
end

When the /1 function is called, it executes the /2 version passing in the default value for that argument. This is helpful to understand so as you auto-complete your functions and see functions listed that you didn’t create, you understand where they are coming from.

Module Names are Atoms Too!

Atoms are a significant part of Elixir. In fact, Elixir modules are atoms!

is_atom(MyFoo)
#=> true

Atom.to_string(MyFoo)
#=> "Elixir.MyFoo"

String.to_atom("Elixir.MyFoo")
#=> MyFoo

:"Elixir.MyFoo" == MyFoo
#=> true

Behind the scenes, an Elixir module is an atom with “Elixir” as part of the name. This namespaces it and makes it easier to identify internally.

Aliases

As you organize your code into modules and namespaces, it can become pretty long. You can use an “alias” to create a name shortcut for your code. Imagine something like the following module.

defmodule MyApp.Customers.Billing.History do

  def compute_for_period(_from_date, _to_date) do
    # compute the value
    103.5
  end

end

In order to execute the function from outside the module, the full namespace is needed. This quickly becomes tedious.

month_total = MyApp.Customers.Billing.History.compute_for_period(month_start, month_end)

An alias can be declared anywhere. However, it is convention to declare the aliases all together at the top of a module.

defmodule MyApp.Customers do
  alias MyApp.Customers.Billing.History

  def compute_current_month() do
    # get the start/end dates for the current month
    History.compute_for_period(month_start, month_end)
  end

end

Be default, the alias name is the last piece of the namespace. In this case, “History”. Any references to “History” inside the module declaring the alias are a shortcut to the full name.

Thinking Tip: Aliases are not Imports

In other languages, you must “import” or “require” code from another file into your current file before you can call it. That is not the case in Elixir. An alias is not an import. It is only a name shortcut. You can use the full namespace name to execute any code available in your application. All public code is available to the application. Code is just a set of instructions. Only private functions are blocked from execution outside of a module.

Override the Alias Name

If the default name would create collisions or be unclear, you can alias it “as” something else to explicitly give it a name.

Let’s define some poorly named modules that will create a name collision when we alias them.

defmodule MyApp.Customers.Orders.Process do
  
  def perform(_order) do
    IO.puts "performing order work"
    :ok
  end

end

defmodule MyApp.Customers.Jobs.Process do
  
  def perform(_job_info) do
    IO.puts "performing job work"
    :ok
  end

end

When I have some code that needs to access both of the above modules, declaring the aliases like this won’t work as expected. No error occurs, but the last alias command wins and the alias to “Process” is overwritten.

alias MyApp.Customers.Orders.Process
#=> MyApp.Customers.Orders.Process

alias MyApp.Customers.Jobs.Process
#=> MyApp.Customers.Jobs.Process

Process
#=> MyApp.Customers.Jobs.Process

An alias that renames the default name might look like this:

alias MyApp.Customers.Orders.Process, as: OrderProcessor
alias MyApp.Customers.Jobs.Process, as: JobProcessor

OrderProcessor.perform(123)
#=> performing order work
#=> :ok

JobProcessor.perform(123)
#=> performing job work
#=> :ok

I don’t recommend module naming like this, but I’ve seen it enough times now that it’s worth mentioning.

Comments are closed

This is a static version of the site. Comments are not available.

6 Comments

  1. Rodrigo Lessa on November 15, 2020 at 7:23 am

    Hey at the end you say you don’t recommend naming modules like this. What would be your recommended way for setting up that module structure?

    • Mark Ericksen on November 15, 2020 at 7:44 am

      Good question! While using an “alias” with :as works, I think it’s simpler and clearer if the module is just named in a way that doesn’t need an :as. Imagine code in different modules using a different alias as and you have to go to the extra effort of looking it up to see if it’s the same one.

      In this example, I think it’s better to just name the module:

      alias MyApp.Customers.Orders.OrderProcessor

  2. Chad Henson on February 19, 2021 at 9:41 am

    It seems like the “alias” feature might be ripe for a decent naming strategy. I could imagine where the namespace represents a functional area of concern and not necessarily just a safeguard against two “same-named” (and arity) functions that do different things. So your function could be called OrderProcessor or you like you mentioned maybe you have a namespace called `MyApp.Orders.Purchasing` and one called `MyApp.Orders.Sales`. One namespace would encompass order processing from the perspective of sales orders and the other would encompass order processing from the perspective of purchase orders. So orders placed to you…versus placed by you. That seems like a reasonable use of aliasing code modules as a means to organize code across functional domains.

  3. Larry Rix on October 27, 2022 at 11:33 am

    It turns out that literally everything in Elixir is a function—that includes modules. You can prove this to yourself by writing:

    defmodule (MyFoo) do

    def (foo) do
    “Hello!”
    end

    end

    Whatever is being used to parse the code is searching for function patterns. One clue is the “do … end” construct, where that is generally preceded by a function name (e.g. “defmodule” or “def” or “defp”) plus a list of argument(s). That there is but a single argument is beside the primary point. In the case above both “MyFoo” and “foo” are arguments being sent in to the defmodule and def functions.

    Note that I added the parenthesis for clarity. Such code does actually compile. For me—know this fact of “everything is a function” has made conditional inclusion of parenthesis make sense (otherwise, I was struggling to understand when and when not to include them).

  4. Larry Rix on October 27, 2022 at 11:48 am

    There is a Principle in Computer Science called Single-entry/Single-exit (SESE) and another school of thought (SEME) Single-entry/Multiple-exit.

    The idea is that exit conditions ought never to break the flow to the end of the function routine, but ought to literally point the way to the end of the routine. Adding to this notion that every Elixir function returns something (even if just nil) is a related matter that has consequences based on either SESE or SEME.

    Over the years, I have grown to enforce SESE even when the language does not enforce it. It helps me to reason about the routine in an expected way. It is another reason to write terse function code rather than verbose. Terse, short functions with a single exit and a forced return or result data item means that you can more easily reason about what the function is doing and why.

    Couple this with functions passing over data rather than data being thrown at functions (as a paradigm) is powerful.

    • Mark Ericksen on October 28, 2022 at 3:46 pm

      I agree! A single exit point is much preferred when reading code. I’ve read large functions in Ruby written by others with many early exists spread throughout the function body. It makes it much harder to reason about.

Comments are closed on this static version of the site.