Photo by Sasha Freemind on Unsplash
Demystifying Elixir Functions: A Comprehensive Guide — Part 4 -Subpart 2/3 of Our Elixir Series
Continuing the series and subpart of part 4, we will look into some other functionality of functions.
6. Parameterized Functions
In Elixir, parameterized functions allow you to create flexible and reusable functions that take arguments. They are a fundamental part of functional programming. Here are a few coding examples to illustrate the concept:
Example 1: Basic Parameterized Function
add_n = fn n -> fn other -> n + other end end
add_two = add_n.(2)
result = add_two.(3) # Returns 5
In this simple example, we define a parameterized function add_n
that takes a value n
and returns another function. We then create a specific function add_two
by invoking add_n
with n
set to 2. Finally, we use add_two
to add 3 to 2.
Example 2: Complex Parameterized Function
divide_by = fn divisor ->
fn dividend when dividend != 0 -> divisor / dividend
dividend -> {:error, "Division by zero"}
end
end
divide = divide_by.(10)
result1 = divide.(2) # Returns 5.0
result2 = divide.(0) # Returns {:error, "Division by zero"}
In this example, we create a more complex parameterised function, divide_by
, which handles division while preventing division by zero. It returns either the result of division or an error tuple.
Example 3: Advanced Parameterised Function
factorial = fn
(0) -> 1
(n) when n > 0 -> n * factorial.(n - 1)
(n) when n < 0 -> {:error, "Factorial undefined for negative numbers"}
end
result1 = factorial.(5) # Returns 120
result2 = factorial.(-2) # Returns {:error, "Factorial undefined for negative numbers"}
In this advanced example, we define a parameterized function factorial
that calculates the factorial of a number. It includes pattern matching to handle various cases, even preventing factorial calculation for negative numbers.
These examples demonstrate the flexibility and power of parameterized functions in Elixir, from simple use cases to handling complex scenarios and errors.
7. Passing Functions as Arguments
Example 1: Applying a Simple Operation
add_one = fn n -> n + 1 end
times_two = fn n -> n * 2 end
apply_operation = fn operation, a, b -> operation.(a, b) end
result1 = apply_operation.(add_one, 5, 2) # Returns 7
result2 = apply_operation.(times_two, 4, 3) # Returns 24
In this example, we define two functions, add_one
and times_two
, each performing a specific mathematical operation. Then, we create an apply_operation
function that takes an operation function and two values, applying the operation to those values. This demonstrates the ability to pass functions as arguments for various operations.
Example 2: Dynamic Function Selection
calculate = fn operation, a, b -> operation.(a, b) end
add = fn (a, b) -> a + b end
subtract = fn (a, b) -> a - b end
result1 = calculate.(add, 10, 4) # Returns 14
result2 = calculate.(subtract, 8, 3) # Returns 5
In this example, the calculate
function takes an operation function as an argument and applies it to two values. We define two operation functions, add
and subtract
, and use the calculate
function to switch between these functions dynamically.
Example 3: Custom Operations
custom_operation = fn n, operation -> operation.(n) end
square = fn n -> n * n end
cube = fn n -> n * n * n end
result1 = custom_operation.(4, square) # Returns 16
result2 = custom_operation.(3, cube) # Returns 27
In this example, the custom_operation
function takes a number n
and an operation function. We define operation functions like square
and cube
to apply custom operations to the provided number n
. This demonstrates the flexibility of passing functions to create custom operations.
Example 4: Functional Composition
double = fn n -> n * 2 end
increment = fn n -> n + 1 end
composed_function = fn x -> double.(increment.(x)) end
result1 = composed_function.(3) # Returns 8
result2 = composed_function.(7) # Returns 16
In this example, we compose functions double
and increment
into a new function composed_function
. The composed function applies the increment
function first and then doubles the result. This showcases the power of passing functions for functional composition.
8. Pinned Values and Function Parameters
Example 1: Customized Greetings
defmodule Greeter do
def for(name, greeting) do
fn (name_to_greet) when name == name_to_greet -> "#{greeting} #{name}"
(_) -> "I don't know you"
end
end
end
greet_mr_valim = Greeter.for("José", "Oi!")
greet_dave = Greeter.for("Dave", "Hello")
result1 = greet_mr_valim.("José") # Returns "Oi! José"
result2 = greet_mr_valim.("Maria") # Returns "I don't know you"
result3 = greet_dave.("Dave") # Returns "Hello Dave"
result4 = greet_dave.("John") # Returns "I don't know you"
In this example, we define the Greeter
module with a function for
that accepts a name and a greeting message. We use a pinned value to customize greetings for specific names. The function matches the input name with the pinned value and generates a greeting accordingly.
Example 2: Handling Different Data Types
defmodule TypeHandler do
def for(:int, action) do
fn (value) when is_integer(value) -> action.(value)
(_) -> "Invalid data type"
end
end
end
int_handler = TypeHandler.for(:int, fn n -> "Received an integer: #{n}" end)
string_handler = TypeHandler.for(:string, fn s -> "Received a string: #{s}" end)
result1 = int_handler.(42) # Returns "Received an integer: 42"
result2 = int_handler.("text") # Returns "Invalid data type"
result3 = string_handler.("Elixir") # Returns "Received a string: Elixir"
result4 = string_handler.(42) # Returns "Invalid data type"
In this example, we create a TypeHandler
module to handle different data types using pinned values. The for
function can customize behavior for specific data types, ensuring that the right action is taken for each input.
Example 3: Restricted Access
defmodule AccessControl do
def for(:admin, action) do
fn (user_type) when user_type == :admin -> action.()
(_) -> "Access denied"
end
end
end
admin_action = AccessControl.for(:admin, fn -> "Admin access granted" end)
guest_action = AccessControl.for(:guest, fn -> "Guest access granted" end)
result1 = admin_action.(:admin) # Returns "Admin access granted"
result2 = admin_action.(:user) # Returns "Access denied"
result3 = guest_action.(:guest) # Returns "Guest access granted"
result4 = guest_action.(:admin) # Returns "Access denied"
In this example, the AccessControl
module allows customized access control based on user types. We use pinned values to handle access rights, ensuring that the appropriate access is granted or denied.
These examples demonstrate how pinned values in function parameters enable the creation of customized functions with different behaviors based on input patterns.
9. The & Notation
Example 1: Simple Arithmetic
add_one = &(&1 + 1)
result1 = add_one.(44) # Returns 45
square = &(&1 * &1)
result2 = square.(8) # Returns 64
In this example, we use the &
operator to create two concise functions. add_one
takes a number and returns the result of adding 1, while square
squares the input number. These compact functions make common arithmetic operations more concise.
Example 2: List Transformation
double_list = &Enum.map(&1, fn x -> x * 2 end)
list = [1, 2, 3, 4]
result3 = double_list.(list) # Returns [2, 4, 6, 8]
capitalize_words = &String.capitalize/1
words = ["elixir", "programming"]
result4 = Enum.map(words, capitalize_words.(&1)) # Returns ["Elixir", "Programming"]
In this example, we demonstrate the use of &
notation for more complex functions. double_list
doubles each element in a list, and capitalize_words
capitalizes a list of words. The concise notation simplifies list transformations and string operations.
Example 3: Custom Function Composition
add_three = &(&1 + 3)
multiply_by_five = &(&1 * 5)
compose = &(&1 |> add_three.() |> multiply_by_five.())
result5 = compose.(7) # Returns 50
Here, we illustrate how the &
operator can be used to compose custom functions. The compose
function combines the add_three
and multiply_by_five
functions to create a custom composition. It applies multiple operations in a single function call.
These examples showcase the versatility of the &
notation for creating concise and custom functions in Elixir.
10. Higher-Order Functions
Example 1: Processing a List
enchanted_items = [
%{title: "Edwin's Longsword", price: 150},
%{title: "Healing Potion", price: 60},
%{title: "Edwin's Rope", price: 30},
%{title: "Dragon's Spear", price: 100}
]
defmodule MyList do
def each([], _function), do: nil
def each([head | tail], function) do
function.(head)
each(tail, function)
end
end
MyList.each(enchanted_items, fn item ->
IO.puts(item.title)
end)
In this example, we have a list of enchanted items, and we define a custom module MyList
. The each/2
function takes a list and a function as arguments. It processes each item in the list by invoking the provided function. In this case, we print the title of each enchanted item. This demonstrates the power of higher-order functions for processing collections in Elixir.
Example 2: Custom Transformation
prices = [150, 60, 30, 100]
double = fn x -> x * 2 end
triple = fn x -> x * 3 end
double_prices = Enum.map(prices, double)
triple_prices = Enum.map(prices, triple)
In this example, we work with a list of prices and create two higher-order functions, double
and triple
, which double and triple a given value, respectively. We use Enum.map/2
to transform the list of prices using these functions. This demonstrates the flexibility of higher-order functions for custom data transformations.
Example 3: Function Composition
add_five = fn x -> x + 5 end
square = fn x -> x * x end
transform = fn f1, f2 -> fn x -> f2.(f1.(x)) end end
add_five_and_square = transform.(add_five, square)
result = add_five_and_square.(3) # Returns 64
In this example, we explore function composition using higher-order functions. We define two functions, add_five
and square
, and create a transform
function that takes two functions and returns their composition. We then compose add_five
and square
into a new function, add_five_and_square
, which applies both operations. This demonstrates the versatility of higher-order functions for building complex functions.
Example 4: Filtering a List
numbers = [1, 2, 3, 4, 5, 6]
is_even = fn x -> rem(x, 2) == 0 end
is_odd = fn x -> rem(x, 2) != 0 end
even_numbers = Enum.filter(numbers, is_even)
odd_numbers = Enum.filter(numbers, is_odd)
In this example, we use higher-order functions to filter a list of numbers. We define is_even
and is_odd
functions to check if a number is even or odd. We then use Enum.filter/2
to create new lists containing only even or odd numbers. This demonstrates how higher-order functions can simplify the process of filtering data.
That’s it for Subpart 2 of 3 subparts of part 4 of this series. Please follow for the last Subpart of this Part 4.