Building Programs with Functions
Explore how to build programs in Elixir by combining small, pure functions that explicitly receive and transform data. Understand immutability, function composition, and using Elixir’s pipe operator to create clear, maintainable code. This lesson introduces core functional programming concepts essential for developing robust Elixir applications.
We'll cover the following...
In functional programming, functions are the primary tools for building a program. We cannot create a useful program without writing or using functions. They receive data, complete some operations, and return a value. They are usually short and expressive.
Multiple little functions are combined to create a larger program. The complexity of building a larger application is reduced when the functions have these properties:
- The values are immutable.
- The function’s result is affected only by the function’s arguments.
- The function doesn’t generate effects beyond the value it returns.
Functions that have these properties are called pure functions. A simple example, in Elixir, is a function that adds 2 to a given number:
This function takes an input, processes it, and returns a value. This is the way most functions work. Some functions will be more complex, their results are unpredictable, and they are known as impure functions. We’ll look at them in chapter 8, Handling Impure Functions.
Using values explicitly
Functional programming always passes the values explicitly between the functions, making it clear to the developer what the inputs and outputs are. The conventional object-oriented languages use objects to store a state, providing methods for operating on that state. The object’s state and methods are very attached to each other. If we change the object’s state, the method invocation will result in a different value. For example, take a look at this Ruby code:
The MySet class doesn’t allow repeated values. When set.push is called, the push method depends on the set object’s internal state. As software evolves, the common tendency is for the object to accumulate more and more internal states. This generates a complex dependency between the methods and the states, which can be hard to debug and maintain. It is therefore essential to be constantly disciplined about applying good practices.
Functional programming gives an alternative. The same MySet example can be used in Elixir to do the same thing differently. Run the following code in the IEx shell below to view the output.
We’ll learn the details of how to create Elixir functions in Chapter 3, Working with Variables and Functions, and structs in Chapter 7, Designing Your Elixir Applications. The most important thing here is that the operations and data are not attached to each other. While in the Ruby example, the operation must be called from a method that belongs to an object that contains data, in Elixir, the operation exists on its own. The data must be explicitly sent to the MySet.push function. Every time we call the function, it generates a new data structure with updated values. Then we update the set variable to store the updated value and print it. The push function works with its arguments and returns a new value.
Using functions in arguments
Functions are so interlaced with everything done in functional programming that they can be used in the arguments and results of functions. Run the following example in the terminal below:
Enum.map(["dogs", "cats", "flowers"], &String.upcase/1)
#Expected Output -> ["DOGS", "CATS", "FLOWERS"]
Here we’re executing a function called Enum.map and passing a list ("dogs", "cats", and "flowers") and a function called String.upcase. The Enum.map function knows how to apply String.upcase to each item in the list. The result is a new list with all words uppercased. Passing functions to other functions is a powerful and mind-blowing mechanism, explored in detail in Chapter 6, Using Higher-Order Functions.
Transforming values
Elixir focuses on the data-transformation flow, and it has a special operator called pipe (|>) to combine multiple functions’ calls and results. Consider code that takes text like “the dark tower” and transforms it into a title, “The Dark Tower.” Instead of writing it like this:
def capitalize_words(title) do
join_with_whitespace(
capitalize_all(
String.split(title)
)
)
end
we can write it like this:
def capitalize_words(title) do
title
|> String.split
|> capitalize_all
|> join_with_whitespace
end
Using the pipe operator, the result of each expression is passed to the next function. (We’ll learn more about it in the lesson Pipelining Your Functions, in Chapter 6.) As we can see, this Elixir function is simple and easy to understand. We can almost read it as plain English. The function capitalize_words receives a title. The title will be split, transforming a list of words. The second transformation will be a list of capitalized words. The final transformation is a unique string with the words separated by whitespace.
In functional programming, every basic building block is a function.
These functions follow principles, such as immutability, that help us build functions that are easier to understand and that are better citizens in the concurrent world.