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.
Contents
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:
- Edit the code in a text file and paste it into IEx
- Edit the code in a text file and execute the file as a script
- 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.
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
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.
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.
6 Comments
Comments are closed on this static version of the site.
Comments are closed
This is a static version of the site. Comments are not available.