Functions and Modules - Introducing Elixir: Getting Started in Functional Programming (2014)

Introducing Elixir: Getting Started in Functional Programming (2014)

Chapter 2. Functions and Modules

Like most programming languages, Elixir lets you define functions to help you represent repeated calculations. While Elixir functions can become complicated, they start out reasonably simple.

Fun with fn

You can create functions in IEx using the keyword fn. For example, to create a function that calculates the velocity of a falling object based on the distance it drops in meters, you could do the following:

iex(1)> fall_velocity= fn (distance) -> :math.sqrt(2 * 9.8 * distance) end

#Function<6.6.111823515/1 in :erl_eval.expr/5>

That binds the variable fall_velocity to a function that takes an argument of distance. (Parentheses are optional around the argument.) The function returns (I like to read the -> as “yields”) the square root of 2 times a gravitational constant for Earth of 9.8 m/s, times distance (in meters). Then the function comes to an end.

The return value in the shell, #Function<6.6.111823515/1 in :erl_eval.expr/5>, isn’t especially meaningful by itself, but it tells you that you’ve created a function and didn’t just get an error. (The exact format of that return value changes with Elixir versions, so it may look a bit different.)

Conveniently, binding the function to the variable fall_velocity lets you use that variable to calculate the velocity of objects falling to Earth:

iex(2)> fall_velocity.(20)

19.79898987322333

iex(3)> fall_velocity.(200)

62.609903369994115

iex(4)> fall_velocity.(2000)

197.9898987322333

If you need to do something more complex, you can separate pieces of your function with newlines. IEx will keep the line open until you type end, as in this more verbose version:

iex(5)> f=fn (distance)

...(5)> -> :math.sqrt(2 * 9.8 * distance)

...(5)> end

iex(6)> f.(20)

19.79898987322333

This can be useful when you want to include multiple statements in a function.

You need the period between the variable name and the argument when you call a function that is stored in a variable. You won’t need it for functions declared in modules, coming later this chapter.

If you want those meters per second in miles per hour, just create another function. You can copy-and-paste the earlier results into it (as I did here), or pick shorter numbers:

iex(6)> mps_to_mph = fn mps -> 2.23693629 * mps end

#Fun<erl_eval.6.111823515>

iex(7)> mps_to_mph.(19.79898987322333)

44.289078952755766

iex(8)> mps_to_mph.(62.609903369994115)

140.05436496173314

iex(9)> mps_to_mph.(197.9898987322333)

442.89078952755773

I think I’ll stay away from 2,000-meter drops. Prefer the fall speed in kilometers per hour?

iex(10)> mps_to_kph = fn(mps) -> 3.6 * mps end

#Fun<erl_eval.6.111823515>

iex(11)> mps_to_kph.(19.79898987322333)

71.27636354360399

iex(12)> mps_to_kph.(62.60990336999411)

225.39565213197878

iex(13)> mps_to_kph.(197.9898987322333)

712.76363543604

You can also go straight to your preferred measurement by nesting the following calls:

iex(14)> mps_to_kph.(fall_velocity.(2000))

712.76363543604

However you represent it, that’s really fast, though air resistance will in reality slow them down a lot.

This is handy for repeated calculations, but you probably don’t want to push this kind of function use too far in IEx, as quitting the shell session makes your functions vanish. This style of function is called an anonymous function because the function itself doesn’t have a name. (The variable name isn’t a function name.) Anonymous functions are useful for passing functions as arguments to other functions. Within modules, though, you can define named functions that are accessible from anywhere.

And the &

Elixir offers a shortcut style for defining anonymous functions using &, the capture operator. Instead of fn, you’ll use &; and instead of naming the parameters, you’ll use numbers, like &1 and &2.

Previously, you defined fall_velocity as:

iex(1)> fall_velocity= fn (distance) -> :math.sqrt(2 * 9.8 * distance) end

#Fun<erl_eval.6.111823515>

If that is too verbose for you, you could use the &:

iex(1)> fall_velocity= &(:math.sqrt(2 * 9.8 * &1))

#Function<6.17052888 in :erl_eval.expr/5>

iex(2)> fall_velocity.(20)

19.79898987322333

When getting started, it’s probably easier to use parameter names, but as impatience sets in, the capture operator is there. Its value will become clearer when you do more complex things with functions, as shown in Chapter 8.

Defining Modules

Most Elixir programs, except things like the preceding simple calculations, define their functions in compiled modules rather than in the shell. Modules are a more formal place to put programs, and they give you the ability to store, encapsulate, share, and manage your code more effectively.

Each module should go in its own file, with an extension of .ex. (You can put more than one module in a file, but keep it simple while getting started.) You should use name_of_module.ex, where name_of_module is the lowercase version of the module name you specify inside of the module file. For the module Drop, the file name would be drop.ex. Example 2-1, which you can find in the examples archive at ch02/ex1-drop, shows what a module, drop.ex, containing the functions previously defined might look like.

Example 2-1. Module for calculating and converting fall velocities

defmodule Drop do

def fall_velocity(distance) do

:math.sqrt(2 * 9.8 * distance)

end

def mps_to_mph(mps) do

2.23693629 * mps

end

def mps_to_kph(mps) do

3.6 * mps

end

end

defmodule contains the functions that the module will support. It takes the name of the module — this time starting with a capital letter — and contains function definitions. These begin with def, using a slightly different structure than you used when defining functions with fn. You don’t need to assign the function to a variable, and use def instead of fn.

NOTE

Function definitions inside of a module must use the longer do… end syntax rather than the shortcut -> syntax. If your function definition is very short, you may put it all on one line like this:

def mps_to_mph(mps), do: 2.23693629 * mps

You may see this “one-liner” version in other people’s code, but for consistency and readability, we recommend that you use the full do…end syntax for all your functions.

Any functions you declare with def will be visible outside of the module and can be called by other code. If you want keep some functions accessible only within the module, you can use defp instead of def, and they will be private.

Usually the code inside of the module will be contained in functions.

How do you make this actually do something?

It’s time to start compiling Elixir code. The shell will let you compile modules and then use them immediately. The c function lets you compile code. You need to start IEx from the same directory as the file you want to compile:

iex(1)> c("drop.ex")

[Drop]

If you were to look at the directory where your drop.ex file is, you would now see a new file named Elixir.Drop.beam. Once compiled, you can use the functions in your module:

iex(2)> Drop.fall_velocity(20)

19.79898987322333

iex(3)> Drop.mps_to_mph(Drop.fall_velocity(20))

44.289078952755766

It works the same as its predecessors, but now you can quit the shell, return, and still use the compiled functions:

iex(4)>

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded

(v)ersion (k)ill (D)b-tables (d)istribution

a

$ iex

Erlang/OTP 17 [erts-6.0] [source-07b8f44] [64-bit] [smp:8:8] [async-threads:10]

[hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.0.0) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> Drop.mps_to_mph(Drop.fall_velocity(20))

44.289078952755766

Most Elixir programming involves creating functions in modules and connecting them into larger programs.

NOTE

If you aren’t sure which directory you are currently in, you can use the pwd() shell command. If you need to change to a different directory, use cd(pathname):

iex(1)> pwd()

/Users/elixir/code/ch02

:ok

iex(2)> cd("ex1-drop")

/Users/elixir/code/ch02/ex1-drop

:ok

iex(3)> pwd()

/Users/elixir/code/ch02/ex1-drop

:ok

When calling named functions, the parentheses are optional. Elixir will interpret a space after the function name as the equivalent of the opening of a set of parentheses, with the parentheses closing at the end of the line. When this produces unexpected results, Elixir may ask in an error message that you “do not insert spaces in between the function name and the opening parentheses.”

NOTE

If you find yourself repeating yourself all the time in IEx, you can also use c to “compile” a series of IEx commands. Instead of defining a module in a .ex file, you put a series of commands for IEx in a .exs (for Elixir script) file. When you call the c function with that file, Elixir will execute all of the commands in it.

ELIXIR COMPILATION AND THE ERLANG RUNTIME SYSTEM

When you write Elixir in the shell, it has to interpret every command, whether or not you’ve written it before. When you tell Elixir to compile a file, it converts your text into something it can process without having to reinterpret all the text, tremendously improving efficiency when you run the code.

That “something it can process,” in Elixir’s case, is an Erlang BEAM file. It contains code that the BEAM processor, a key piece of the Erlang Runtime System (ERTS), can run. BEAM is Bogdan’s Erlang Abstract Machine, a virtual machine that interprets optimized BEAM code. This may sound slightly less efficient than the traditional compilation to machine code that runs directly on the computer, but it resembles other virtual machines. (Oracle’s Java Virtual Machine (JVM) and Microsoft’s .NET Framework are the two most common virtual machines.)

Erlang’s virtual machine optimizes some key things, making it easier to build applications that scale reliably. Its process scheduler simplifies distributing work across multiple processors in a single computer. You don’t have to think about how many processors your application might get to use — you just write independent processes, and Erlang spreads them out. Erlang also manages input and output in its own way, avoiding connection styles that block other processing. The virtual machine also uses a garbage collection strategy that fits its style of processing, allowing for briefer pauses in program execution. (Garbage collection releases memory that processes needed at one point but are no longer using.)

When you create and deliver Elixir programs, you will be distributing them as a set of compiled BEAM files. You don’t need to compile each one from the shell as we’re doing here, though. elixirc will let you compile Elixir files directly and combine that compilation into make tasks and similar things, and calling elixir on .exs files will let you run Elixir code as scripts outside of the IEx environment.

From Module to Free-Floating Function

If you like the style of code that fn allowed but also want your code stored more reliably in modules where it’s easier to debug, you can get the best of both worlds by using &, the capture operator to refer to a function you’ve already defined. You can specify the function to retrieve with a single argument in the form Module_name.function_name/arity. Arity is the number of arguments a function takes: 1 in the case of Drop.fall_velocity:

iex(2)> fun=&Drop.fall_velocity/1

&Drop.fall_velocity/1

iex(3)> fun.(20)

19.79898987322333

You can also do this within code in a module. If you’re referring to code in the same module, you can leave off the module name preface. In this case, that would mean leaving off Drop. and just using &(fall_velocity/1).

Splitting Code Across Modules

The Drop module currently mixes two different kinds of functions. The fall_velocity function fits the name of the module, Drop, very well, providing a calculation based on the height from which an object falls. The mps_to_mph and mps_to_kph functions, however, aren’t about dropping. They are generic measurement-conversion functions that are useful in other contexts and really belong in their own module. Example 2-2 and Example 2-3, both in ch02/ex2-split, show how this might be improved.

Example 2-2. Module for calculating fall velocities

defmodule Drop do

def fall_velocity(distance) do

:math.sqrt(2 * 9.8 * distance)

end

end

Example 2-3. Module for converting fall velocities

defmodule Convert do

def mps_to_mph(mps) do

2.23693629 * mps

end

def mps_to_kph(mps) do

3.6 * mps

end

end

Next, you can compile them, and then the separated functions are available for use:

Interactive Elixir (1.0.0) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> c("drop.ex")

[Drop]

iex(2)> c("convert.ex")

[Convert]

iex(3)> Drop.fall_velocity(20)

19.79898987322333

iex(4)> Convert.mps_to_mph(Drop.fall_velocity(20))

44.289078952755766

That reads more neatly, but how might this code work if a third module needed to call those functions? Modules that call code from other modules need to specify that explicitly. Example 2-4, in ch02/ex3-combined, shows a module that uses functions from both the drop and convert modules.

Example 2-4. Module for combining drop and convert logic

defmodule Combined do

def height_to_mph(meters) do

Convert.mps_to_mph(Drop.fall_velocity(meters))

end

end

That looks much like the way you called it from IEx. This will only work if the Combined module has access to the Convert and Drop modules, typically by being in the same directory, but it’s quite similar to what worked directly in IEx.

The combined function lets you do much less typing:

iex(5)> c("combined.ex")

[Combined]

iex(6)> Combined.height_to_mph(20)

44.289078952755766

NOTE

If you’re coming from Erlang, you’re probably used to the discipline of thick module walls and functions that only become accessible through explicit -export and -import directives. Elixir goes the opposite route, making everything accessible from the outside except for functions explicitly declared private with defp.

Combining Functions with the Pipe Operator

There’s another way to combine the functions, using Elixir’s |> operator, called the pipe operator. The pipe operator, sometimes called pipe forward, lets you put the result of one function into the first argument of the next function. Example 2-5, in ch02/ex4-pipe, shows the operator in use.

Example 2-5. Using the pipe operator

defmodule Combined do

def height_to_mph(meters) do

Drop.fall_velocity(meters) |> Convert.mps_to_mph

end

end

Note that the order is reversed from Example 2-4, with Drop.fall_velocity(meters) appearing before Convert.mps_to_mph. If you read |> as “goes into”, the logic may be clearer. You can have several of these in a row, converting functions that used to be deeply nested into hopefully clearer sequences.

NOTE

The pipe operator only passes one result into the next function as its first parameter. If you need to use a function that takes multiple parameters, just specify the additional parameters as if the first didn’t have to be there.

Importing Functions

As long as you fully specify the name of the function, Elixir does a great job of seeking out the code. However, if you’re working with code that relies on code in a particular module constantly, it may be appealing to reduce your typing by formally importing it.

Example 2-6, in ch02/ex5-import, shows a simple use of import to bring in all the functions (and macros, though there aren’t any yet) in the Convert module.

Example 2-6. Module for combining drop and convert logic, with imported Convert

defmodule Combined do

import Convert

def height_to_mph(meters) do

mps_to_mph(Drop.fall_velocity(meters))

end

end

The import Convert line tells Elixir that all of the functions and macros (except those starting with underscore) in the Convert module should be available without prefixes in this module.

Importing an Erlang module, shown in Example 2-7, is much the same, except that you prefix the module name with a colon and don’t start the name with an uppercase letter:

Example 2-7. Importing the Erlang math module.

defmodule Drop do

import :math

def fall_velocity(distance) do

sqrt(2 * 9.8 * distance)

end

end

Importing entire modules might create conflicts with function names you are already using in your own module, so Elixir lets you specify which functions you want with the only argument. For example, to get just the sqrt function, you could use:

defmodule Drop do

import :math, only: [sqrt: 1]

def fall_velocity(distance) do

sqrt(2 * 9.8 * distance)

end

end

If you just need to import a module for one function, you can place the import directive inside of the def or defp for that function. It won’t apply beyond that function’s scope:

defmodule Drop do

def fall_velocity(distance) do

import :math, only: [sqrt: 1]

sqrt(2 * 9.8 * distance)

end

end

NOTE

If you want all of the functions except for some specific functions, you can use the except argument:

import :math, except: [sin: 1, cos:, 1]

Use import with caution. It certainly spares you typing, but it can also make it harder to figure out where functions came from.

Default Values for Arguments

If you wanted to deal with astronomical bodies other than Earth (and you’ll be doing a lot of that in subsequent chapters), you might want to create a fall_velocity/2 function that accepts both a distance and a gravity constant:

defmodule Drop do

def fall_velocity(distance, gravity) do

:math.sqrt(2 * gravity * distance)

end

end

You can then calculate velocities from Earth, where the gravity constant is 9.8, and the moon, where the gravity constant is 1.6:

iex(1)> c("drop.ex")

[Drop]

iex(2)> Drop.fall_velocity(20, 9.8)

19.79898987322333

iex(3)> Drop.fall_velocity(20, 1.6)

8.0

If you anticipate dropping objects primarily on Earth, Elixir lets you specify a default value for the gravity parameter by putting the default value after a pair of backslashes, as in Example 2-8, which you can find in ch02/ex6-defaults.

Example 2-8. Function with a default value

defmodule Drop do

def fall_velocity(distance, gravity \\ 9.8) do

:math.sqrt(2 * gravity * distance)

end

end

Now you can specify only the first argument for Earth, and both arguments for other astronomical bodies:

iex(4)> c("drop.ex")

drop.ex:1: warning: redefining module Drop

[Drop]

iex(5)> Drop.fall_velocity(20)

19.79898987322333

iex(6)> Drop.fall_velocity(20, 1.6)

8.0

Documenting Code

Your programs can run perfectly well without documentation. Your projects, however, will have a much harder time.

While programmers like to think they write code that anyone can look at and sort out, the painful reality is that code even a little more complicated than that shown in the previous examples can prove mystifying to other developers. If you step away from code for a while, the understanding you developed while programming it may have faded, and even your own code can seem incomprehensible.

Elixir’s creators are well aware of these headaches and have emphasized “Documentation as first-class citizen” right on the front page of Elixir’s website (for now at least!).

The simplest way to add more explicit explanations to your code is to insert comments. You can start a comment with #, and it runs to the end of the line. Some comments take up an entire line, while others are short snippets at the end of a line. Example 2-9 shows both varieties of comments.

Example 2-9. Comments in action

#convenience functions!

defmodule Combined do

def height_to_mph(meters) do #takes meters, returns mph

Convert.mps_to_mph(Drop.fall_velocity(meters))

end

end

The Elixir compiler will ignore all text between the # sign and the end of the line, but humans exploring the code will be able to read them.

Informal comments are useful, but developers have a habit of including comments that help them keep track of what they’re doing while they’re writing the code. Those comments may or may not be what other developers need to understand the code, or even what you need when you return to the code after a long time away. More formal comment structures may be more work than you want to take on in the heat of a programming session, but they also force you to ask who might be looking at your code in the future and what they might want to know.

Elixir goes way beyond basic comments, offering a set of tools for creating documentation you can explore through IEx or separately through a web browser.

Documenting Functions

The Drop module contains one function: fall_velocity/1. You probably know that it takes a distance in meters and returns a velocity in meters per second for an object dropped in a vacuum on Earth, but the code doesn’t actually say that. Example 2-10 shows how to fix that with the@doc tag.

Example 2-10. Documented function for calculating fall velocities

defmodule Drop do

@doc """

Calculates the velocity of an object falling on Earth

as if it were in a vacuum (no air resistance). The distance is

the height from which the object falls, specified in meters,

and the function returns a velocity in meters per second.

"""

def fall_velocity(distance) do

import :math, only: [sqrt: 1]

sqrt(2 * 9.8 * distance)

end

end

After you compile that, the h function in IEx will now tell you useful information about the function:

iex(1)> c("drop.ex")

[Drop]

iex(2)> h Drop.fall_velocity

* def fall_velocity(distance)

Calculates the velocity of an object falling on Earth

as if it were in a vacuum (no air resistance). The distance is

the height from which the object falls, specified in meters,

and the function returns a velocity in meters per second.

That’s a major improvement, but what if a user specifies “twenty” meters instead of 20 meters? Because Elixir doesn’t worry much about types, the code doesn’t say that the value for distance has to be a number or the function will return an error.

You can add a tag, @spec, to add that information. It’s a little strange, as in some ways it feels like a duplicate of the method declaration. In this case, it’s simple, as shown in Example 2-11.

Example 2-11. Documented function for calculating fall velocities

defmodule Drop do

@doc """

Calculates the velocity of an object falling on Earth

as if it were in a vacuum (no air resistance). The distance is

the height from which the object falls, specified in meters,

and the function returns a velocity in meters per second.

"""

@spec fall_velocity(number()) :: number()

def fall_velocity(distance) do

import :math, only: [sqrt: 1]

sqrt(2 * 9.8 * distance)

end

end

Now you can use the s function to see type information about your function from IEx:

iex(3)> s(Drop.fall_velocity)

@spec fall_velocity(number()) :: number()

You can also use s(Drop) to see all the specs defined in the Drop module.

This chapter has really demonstrated only the number() type, which combines integer() and float(). Appendix A includes a full list of types.

Documenting Modules

The modules in this chapter are very simple so far, but there is enough there to start documenting, as shown in the files at ch02/ex7-docs. Example 2-12 presents the Drop module with more information about who created it and why.

Example 2-12. Documented module for calculating fall velocities

defmodule Drop do

@moduledoc """

Functions calculating velocities achieved by objects dropped in a vacuum.

from *Introducing Elixir*, O'Reilly Media, Inc., 2014.

Copyright 2014 by Simon St.Laurent and J. David Eisenberg.

"""

@vsn 0.1

@doc """

Calculates the velocity of an object falling on Earth

as if it were in a vacuum (no air resistance). The distance is

the height from which the object falls, specified in meters,

and the function returns a velocity in meters per second.

"""

@spec fall_velocity(number()) :: number()

def fall_velocity(distance) do

import :math, only: [sqrt: 1]

sqrt(2 * 9.8 * distance)

end

end

This lets you use h to learn more about the module:

iex(4)> h Drop

Drop

Functions calculating velocities achieved by objects dropped in a vacuum.

from *Introducing Elixir*, O'Reilly Media, Inc., 2014. Copyright 2014 by

Simon St.Laurent and J. David Eisenberg.

Having the documentation is useful for anyone else who is reading your code (and if you are away from your code for a few months, when you return, you will be that “anyone else”). You can also use this documentation to create web pages that summarize your modules and functions. To do this, you need the ExDoc tool. ExDoc recognizes Markdown formatting in your documentation, so your documentation can include emphasized text, lists, and links, among other things. For more details on using ExDoc, see Appendix B.