Part 1 of this topic introduced modules, functions, and how to call them. There's a lot to talk about with functions in Elixir, so let's continue.
Default Parameter Values
Elixir function parameters can be given default values using \\
after each parameter.
def do_something(item, count \\ 1)
So in the above function, if you don't specify a count
parameter when calling it, the count
parameter will default to 1. This means you can call do_something("Bob")
, in which case count
will default to 1 or do_something("Bob", 4)
, in which case count
will get a value of 4.
Elixir will actually generate multiple function definitions when it sees default values. In this case, it generates do_something/1
and do_something/2
. The function with an arity of 1 will just call the function with an arity of 2, passing it the default value.
That was all the information I found when learning about default parameter values, so I set out to learn about how default values differ compared to Javascript and C#.
In Javascript, all parameters are optional. If you specify a default value for a parameter (ES6 and later), that default value will be assigned to the parameter if one wasn't specified by the caller. If not, then that parameter will get the value undefined
.
Let's look at the following function in Javascript.
function do_something(a, b = 1, c, d) {
return [a, b, c, d];
}
Calling do_something("Bob")
will return ["Bob", 1, undefined, undefined]
. The parameters that had neither default values nor had values supplied by the caller will default to undefined
.
In C#, on the other hand, all parameters with default values have to be at the end of the parameter list. A parameter with a default value, which is an optional parameter in C#, can never come before a required parameter (one that has no default value).
So the following example would result in a compile error.
public List<int> DoSomething(int a, int b = 1, int c, int d)
{
return new List<int> {a, b, c, d};
}
Moving the default value to the end would allow the code to compile.
public List<int> DoSomething(int a, int b, int c, int d = 1)
{
return new List<int> {a, b, c, d};
}
I didn't find any indication of how Elixir would behave in this situation after about 30 seconds of searching, so I decided to try it out to see what Elixir would do. Elixir didn't do what I would expect.
I created the following function in Elixir
def create_list(a, b \\ 1, c, d) do
[a, b, c, d]
end
I then called it with three parameters. My expectation was that it would fill in the first three (a, b, c), and then give me an error because it didn't have a value for d. Instead it went like this:
iex> DefaultValues.create_list("Bob", 4, 3)
{"Bob", 1, 4, 3}
iex> DefaultValues.create_list("Bob", :ok, 4, 3)
{"Bob", :ok, 4, 3}
It turns out that Elixir generated a do_something/3
function and a do_something/4
function. do_something/3
only took the required parameters and called do_something/4
with the default value for the second parameter. That was very interesting. It has the effect that we can put the default values anywhere in the parameter list and then only pass the required parameters, regardless of where they occur in the parameter list. The required parameters will be filled first.
It makes sense once I realize that Elixir generates alternate function definitions that call the real definition with the default values. It's irrelevant where the optional parameters are located.
Private Functions
A private function is a function in a module that is only visible within the module. It cannot be be referenced or called from outside the module. Whereas public functions are defined using the def
keyword, private functions are defined using the defp
keyword.
defmodule ExampleModule do
defp private_function(value) do
end
end
Function and Module Naming Conventions
Functions and modules have naming conventions that every Elixir developer uses. Elixir won't get upset if you go outside those conventions, but other Elixir developers will find it more difficult to read and understand your code.
Like variables, functions should be snake cased. This where they are named using all lower-case letters with underscores beween the words, like do_something_unexpected
.
Like with a variable, a function name that begins with an underscore (_) indicates that a function should not be used. I'm not sure why functions are defined this way rather than being defined as private functions, but there must be some valid uses cases I'm not aware of. Perhaps it's done this way so that one particular module can use it (a bit like the concept of friends in C++), but not other modules that have no business doing so? I'd be interested in seeing an example of this and the reasoning for it.
A "?" character is typically added to the end of a function name when it returns a boolean value. For example: is_available?
. An exception to this are functions that can be used in guard clauses. Those functions return a boolean variable, but do not have a "?" at the end of the function name. I don't know what guard clauses are at this point, but it's something I'll learn in the future.
A "!" character is typically added to the end of a function if it raises an exception when there's a failure. Many functions in the Elixir I/O libraries have a version of a function that raises an exception on failure and a version of the same function that does not raise an exception, with the difference between the two functions being the "!" character at the end of the function name.
For example, File.write/3
and File.write!/3
.
The functions that do not throw an exception will typically return a tuple that contains the result of the operation (:ok
or :error
) and any data that resulted from the operation.
When "size" appears in an Elixir function name, that indicates that the operation runs in constant time (O(1)). This is usually because the size is stored with the data structure.
When "length" appears in an Elixir function name, that indicates that the operation runs in linear time (O(n)). This is usually because the algorithm has to iterate over the data items to figure out how many there are.
When a function parameter will not be used, and underscore is placed in front of the name. It's common to have several versions of a function where a parameter is used in one version, but not another. Not only does the underscore impart information to the human reading the code, but it also suppresses a compile warning that is emitted when a function does not use a parameter.
An example warning when the compiler detects an unused parameter:
warning: variable "param2" is unused
iex:4
By putting an underscore before the unused parameter(s), this warning can be avoided.
def do_something(param1, _param2) do
param1
end
Modules are named using camel case, where the first letter is a capital letter and each word in the module starts with a capital letter.
defmodule RoadSegment do
end
Using Functions from Another Module
Calling a function in another module can simply be done by calling "[Module Name].[Function Name]".
Here's an example of calling the length
function in the String
module. The String module is part of the Elixir standard library, and it appears that all standard library modules are available to use everywhere.
iex> String.length("This is a string")
16
At this point, it's not yet clear to me how Elixir finds the module you referenced. Is there some kind of search path or are all the modules that have been compiled by the Elixir compiler available for use? I don't know. I expect that will become more clear in the future.
Importing Entire Modules
If you want to import the functions from a module into the current namespace, you can use the import
keyword. The following example imports all the functions from the String module into the current namespace.
iex> import String
String
iex> String.upcase("This is a string")
"THIS IS A STRING"
iex> upcase("This is a string")
"THIS IS A STRING"
Importing a function means that you don't have to specify its module name anymore. You can just call its function name.
The functions that are available in Elixir without specifying a module name or importing anything are located in the Kernel
module. These are functions such as is_atom
or div
. Even operators such as +
, -
, &&
or ===
are actually functions in the Kernel
module. These are available without specifying Kernel.+
or Kernel.is_atom
because the Elixir environment automatically imports all the functions in Kernel
.
You'll need to be careful because the names of functions in a module that were imported into the current namespace can conflict with functions from other modules that were imported.
For example, the Kernel
module has a function called length/1
that determines the length of a list. Why this is not in the List module instead is a mystery to me, but that's the way it is. (I notice that the documentation indicates that this can be used in guard clauses, whatever those are, so I wonder if that's the reason). The String
module also has a function called length/1
. I noticed that importing all the functions in String didn't cause any kind of error or warning, yet we should now have two length/1
functions in the current namespace.
Let's see what happens when we try to call length/1
on a string.
iex> import String
String
iex> length("This is a string")
** (CompileError) iex:7: function length/1 imported from both String and Kernel, call is ambiguous
(elixir) src/elixir_dispatch.erl:111: :elixir_dispatch.expand_import/6
(elixir) src/elixir_dispatch.erl:81: :elixir_dispatch.dispatch_import/5
iex> String.length("This is a string")
16
iex> Kernel.length([1, 2, 3])
3
That is interesting. Elixir complains that the call is ambiguous because both String.length/1
and Kernel.length/1
have been imported into the current namespace. We have to disambiguate them by specifying the module name.
Note that Elixir isn't like Python, where you have to use import
in order to load and use another module. It's more like the using
statement in C#, which just brings code from another namespace into the current namespace. Like C#, Elixir code can still still use that other code without any sort of import
statement, but importing it will allow the developer to refer to that code without typing in the entire namespace.
Any functions in a moduel starting with an underscore will not be automatically imported, because by convention, these are functions that should not be used. If you must use them anyway, you have to specifically import them by name.
Importing Specific Functions from a Module
Elixir guidelines discourage importing all the functions in a module due to the likelihood of function name conflicts and creating unclear code where someone who's reading the code isn't certain which module a particular function actually came from. Instead, the guidelines recommend either specifying the module name or importing specific functions by name from modules, which will make it clear exactly which functions are being imported and which module they are being imported from.
We can import specific functions from a module using only:
syntax. Let's look at an example where we import a couple functions from the String
modules.
iex> import String, only: [upcase: 1, trim: 1]
String
iex> upcase("This is a string")
"THIS IS A STRING"
iex> trim(" This is a string ")
"This is a string"
iex> split("This is a string")
** (CompileError) iex:14: undefined function split/1
iex> String.split("This is a string")
["This", "is", "a", "string"]
iex> length([1, 2, 3])
3
In this example, we imported only upcase/1
and trim/1
from the String modules. The upcase: 1
value in the import statement corresponds to upcase/1
. We were then able to use both the imported functions, but another function in the String
module that was not imported, String.split/1
, is still unavailable in the current namespace.
Importing specific functions from String
avoids the length/1
function name conflict, because we didn't import String.length/1
. Importing functions this way also makes it clear where the upcase/1
and trim/1
functions came from.
There's also except:
, which is used in the same context as only:
, but with the opposite effect. It imports all functions except for the ones listed.
Other Ways of Importing
Like the "using" statement in C#, you can create a namespace alias instead of importing a module into the current namespace. This is done with the alias
keyword.
iex> alias String, as: Bob
String
iex> Bob
String
iex> Bob.length("This is a string")
16
iex> Bob.upcase("This is a string")
"THIS IS A STRING"
iex> String.upcase("This is a string")
"THIS IS A STRING"
In this example, I created an alias called Bob
that is an alias for the String
module.
You can use alias
to simplify long namespaces, so a namespace such as NameSpace1.Namespace2.Category.Subdivision.ExampleModule
can be aliased to ExampleModule
. This makes it a lot easier to refer to a function in that module without importing its functions into the current namespace.
If you use alias
without the as:
, the alias will be set to the last part of the module name. So alias NameSpace1.Namespace2.Category.Subdivision.ExampleModule
without as:
would automatically be aliased to ExampleModule
, since that is the last part of the module name following the last dot.
Both an alias and an import are lexically-scoped, so you can put aliases and imports at the file level, inside modules, or inside individual functions if you want to do so. An alias or import contained within a function would only apply in the scope of that function and be unavailable in other functions in the same module.
defmodule ExampleModule do
def atom_to_string_one(atom) do
Atom.to_string(atom)
end
def atom_to_string_two(atom) do
import Atom, only: [to_string: 1]
to_string(atom)
end
def atom_to_string_three(atom) do
alias Atom, as: Waffle
Waffle.to_string(atom)
end
end
There is a use
macro that will import a module's functions into the current namespace, but also runs some code in that module that will do some setup work that needs to be done before the module is used. At this point, I don't know exactly what this involves, how to use it, or even what a macro is in the context of Elixir, but I do know that it is a tool that is available. I expect to learn more about the use
macro, and macros in general, once I get into learning the more advanced functionality.
Module Attributes
A module attribute represents an annotation containing some kind of metadata or data that can be used as a constant. A module attribute begins with an "@" character and a lower-case letter.
defmodule ExampleModule do
@test_file_name "test_file.txt"
def test_file() do
@test_file_name
end
end
This example defines an attribute called @test_file_name
and assigns it a value "test_file.txt"
. This attribute can then be used anywhere within the module. Here I use the attribute like a constant. The attribute doesn't appear to be available outside the module.
iex> ExampleModule.test_file
"test_file.txt"
iex> ExampleModule.@test_file_name
** (CompileError) iex:23: undefined function test_file_name/0
Module attributes can be redefined and the compiler will use whichever value is currently assigned as it goes down through the code. This also means that the same attribute can be reused for metadata purposes without causing trouble.
defmodule ExampleModule do
@test_file_name "test_file.txt"
def test_file() do
@test_file_name
end
@test_file_name "another_test_file.txt"
def second_test_file(), do: @test_file_name
end
iex> ExampleModule.test_file
"test_file.txt"
iex> ExampleModule.second_test_file
"another_test_file.txt"
In these examples, I'm not using parentheses to make the function call. A function that just returns a constant feels more like a module constant to me (even though it's actually a function), so I'm just leaving off the parentheses. Elixir is just fine with that.
Modules attributes can only be defined at the module level, not inside functions.
I don't feel I've learned a lot about how attributes are used yet. I know that they're often used as annotations on modules and functions, but I suspect that I have yet to get a sense of how they are used in real Elixir code. I expect to learn more about how they are typically used later on when I look at real Elixir code.
I do know that module attributes are only available at compile time, and cannot be retrieved at runtime by querying metadata. So any code that uses an attribute value will have that value inserted during compilation.
ElixirOperator - A Practical Example
I think I've learned enough now to do something practical. I learn best by taking what I've learned and doing something with it, so I'm going to build something simple now. I encourage you to take the time to do something similar. Do some coding with what you know so far from following along with me. Be curious and play around with Elixir. It will teach you much more than just reading this and doing nothing else.
You can find the ElixirOperator project in its own folder under the projects folder in the Learn With Me: Elixir repository on Github. You can download the project and follow along.
First let's look at the MathOperations
module found in math_operations.exs.
defmodule MathOperations do
@pi 3.141592653589793
def add(x, y), do: x + y
def subtract(x, y), do: x - y
def multiply(x, y), do: x * y
def divide(x, y), do: x / y
def mod(x, y), do: rem(x, y)
def negate(x), do: -x
def square(x), do: x * x
def pi(), do: @pi
end
This module contains a variety of mathematical functions. The add
, subtract
, multiply
, and divide
functions are pretty straightforward. The mod
function performs a modulo operation, which is represented by the %
operator in C# and Javascript. The negate
function takes a number and flips its sign, the square
function squares a number, and the pi
function simply returns the value of PI, which is stored in a module attribute. It's a pretty simple module overall, and all functions can be written as single-line functions.
Now let's load this module into IEx using the c
function. The path is relative to the current directory. To make it easy, go to the ElixirOperator directory and start IEx.
iex> c "math_operations.exs"
[MathOperations]
Now we have the module loaded into IEx and we can start calling functions.
iex> MathOperations.add(10, 23)
33
iex> MathOperations.subtract(5, 15)
-10
iex> MathOperations.multiply(5, 3)
15
iex> MathOperations.divide(10, 3)
3.3333333333333335
iex> MathOperations.mod(8, 2)
0
iex> MathOperations.mod(8, 3)
2
iex> MathOperations.negate(9)
-9
iex> MathOperations.negate(-17)
17
iex> MathOperations.square(5)
25
iex> MathOperations.pi
3.141592653589793
iex> MathOperations.pi()
3.141592653589793
Great! I created a module with some useful math operations. It's not really that impressive, but it was a good exercise to get familiar with Elixir.
I also wanted to get some practice creating some higher-order functions, which led to me creating the Operator module. The functions in the operator module receive one or more functions and some data and call those functions with that data.
Let's take a look.
defmodule Operator do
#Takes some data and a function with one parameter and applies a function to that data
def apply(operation, data) do
operation.(data)
end
#Takes some data and a function with two parameters and applies a function to that data
def apply(operation, data1, data2) do
operation.(data1, data2)
end
#Takes some data and two functions, the first function with one parameter and the second function with
#one parameter and applies the first function to that data, with the second function fed the result
#of the first function. The result of that will be returned.
def apply_two(operation1, operation2, data) do
result = operation1.(data)
operation2.(result)
end
#Takes some data and two functions, the first function with two parameters and the second function with
#one parameter and applies the first function to that data, with the second function fed the result
#of the first function. The result of that will be returned.
def apply_two(operation1, operation2, data1, data2) do
result = operation1.(data1, data2)
operation2.(result)
end
end
Yes, the same thing can be done by calling the functions directly, but this way I'll get some practice with implementing higher-order functions.
The apply functions will call a single function, passing it some data and returning the result. The apply_two functions will pass the data to the first function, take the result of the first function and pass it to the second function, and then return the result of the second function. It's like a primitive, inflexible function pipeline.
initial data --> function1 --> function2 --> final result
See how we use dot notation in the Operator module to call the functions passed into us. This is because those functions are bound to a parameter and are not named functions. It's possible that the parameter variable may be bound to a named function, but it is not itself a named function.
Let's load the module into IEx and start using it in conjunction with the functions from the MathOperations module. This is an example of function composition where we can combine simpler functions to do something more complex.
iex> c "operator.exs"
[Operator]
iex> Operator.apply(&MathOperations.add/2, 4, 9)
13
iex> Operator.apply(&MathOperations.subtract/2, 4, 9)
-5
iex> Operator.apply(&MathOperations.multiply/2, 4, 9)
36
iex> Operator.apply(&MathOperations.divide/2, 4, 9)
0.4444444444444444
iex> Operator.apply(&MathOperations.mod/2, 9, 4)
1
iex> Operator.apply(&MathOperations.negate/1, 9)
-9
iex> Operator.apply(&MathOperations.square/1, 9)
81
iex> Operator.apply(&MathOperations.square/1, MathOperations.pi)
9.869604401089358
Notice how we have to use the capture operator and the function's name/arity to pass a function to the apply method. In the last call, we passed the square/1 function and we called the pi/0 function to retrieve the data to be squared. The result was that PI was squared. If we had wanted to pass the MathOperations.pi/0
function rather than the value it returns, we would have had to use the capture operator. That's the difference in syntax between passing a function and calling the function and passing its result.
We are also calling both versions of the apply function here. The calls with three parameters match Operator.apply/3
and the calls with two parameters match Operator.apply/2
.
Now let's try using the apply_two
functions.
iex> Operator.apply_two(&MathOperations.negate/1, &MathOperations.square/1, 8)
64
iex> Operator.apply_two(&MathOperations.square/1, &MathOperations.negate/1, 8)
-64
iex> Operator.apply_two(&MathOperations.add/2, &MathOperations.square/1, 5, 2)
49
iex> Operator.apply_two(&MathOperations.multiply/2, &MathOperations.square/1, 5, 2)
100
- In the first call, the number 8 is negated to -8 and then squared, which results in the value 64.
- In the second call, we reversed the order of operations so that 8 is squared to 64 and then negated, which results in -64.
- In the third call, 5 and 2 are added to get 7, which is then squared. The result is 49.
- In the fourth call, 5 and 2 are multiplied to get 10, which is then squared. The result is 100.
I've gotten the impression that functional programming is filled with higher-order functions that implement some kind of pattern and then more specific functions are passed into them as parameters. This is a simple example of that way of programming. I expect I'll get better at this way of thinking over time.
Before we finish, let's try passing some anonymous functions to these apply
functions.
iex> Operator.apply(fn (x, y) -> x - (y *2) end, 10, 3)
4
iex> Operator.apply(fn x -> x * (x + 1) end, 10)
110
iex> Operator.apply(&(&1 + (&2 * &1)), 10, 3)
40
iex> Operator.apply_two(&(&1 + (&2 * &1)), &MathOperations.negate/1, 10, 3)
-40
- The first call applies an anonymous function with two parameters
- The second call applies an anonymous function with one parameter
- The third call uses the shortcut syntax to create an anonymous function
- The fourth call applies an anonymous function and then uses the
&MathOperations.negate/1
function to negate the result.
Well, that little project taught me a lot. What I didn't show here are the various compile errors and the other errors IEx showed me while I attempting to call these functions. There were indeed plenty of errors, but I didn't think that would be very helpful to clutter up these code examples with a lot of errors, so I edited them out.
Here's the unedited version of that last example.
iex> Operation.apply(fn (x, y) -> x - (y *2) end, 10, 3)
** (UndefinedFunctionError) function Operation.apply/3 is undefined (module Operation is not available)
Operation.apply(#Function<12.99386804/2 in :erl_eval.expr/5>, 10, 3)
iex> Operator.apply(fn (x, y) -> x - (y *2) end, 10, 3)
4
iex> Operator.apply(fn x -> x * (x + 1), 10, 3)
** (SyntaxError) iex:69: unexpected token: ). The "fn" at line 69 is missing terminator "end"
iex> Operator.apply(fn x -> x * (x + 1) end, 10, 3)
** (BadArityError) #Function<6.99386804/1 in :erl_eval.expr/5> with arity 1 called with 2 arguments (10, 3)
projects/ElixirOperator/operator.exs:9: Operator.apply/3
iex> Operator.apply(fn x -> x * (x + 1) end, 10)
110
iex> Operator.apply(&(&1 + (&2 * &1)), 10)
** (BadArityError) #Function<12.99386804/2 in :erl_eval.expr/5> with arity 2 called with 1 argument (10)
projects/ElixirOperator/operator.exs:4: Operator.apply/2
iex> Operator.apply(&(&1 + (&2 * &1)), 10, 3)
40
iex> Operator.apply(&(&1 + (&2 * &1)), &MathOperations.negate/1, 10, 3)
** (UndefinedFunctionError) function Operator.apply/4 is undefined or private. Did you mean one of:
* apply/2
* apply/3
Operator.apply(#Function<12.99386804/2 in :erl_eval.expr/5>, &MathOperations.negate/1, 10, 3)
iex> Operator.apply_two(&(&1 + (&2 * &1)), &MathOperations.negate/1, 10, 3)
-40
iex>
The first error was me getting the name of the Operator
module wrong. The second error was me forgetting to terminate the anonymous function with end
. The third error was me applying two parameters to a function that only accepted one parameter. The fourth error was me attempting to apply one parameter to a function that accept two parameters. The fifth error was me passing two functions to the apply
functions instead of the apply_two
functions.
I want you to know that it's common to make a lot of mistakes while learning, and failing is all a part of learning! The process of stumbling around and causing errors and figuring what went wrong was an essential part of learning Elixir. It also helps gives you practice in interpreting Elixir error messages, making it much easier for you to figure out what went wrong in the future. As long as you learn something from your mistakes, it's not a waste of time.
I learned a lot more by doing this than if I had just kept reading about it. I encourage you to take a break from reading this at this point and play around with Elixir, using what we've learned so far. It's quite an effective way to learn.