Learning Elixir: Anonymous Functions

Anonymous functions are a cornerstone of functional programming in Elixir, allowing you to create functions on the fly without naming them. They're versatile building blocks that you can assign to variables, pass as arguments, or return from other functions. This article explores how to create and use anonymous functions effectively in your Elixir projects. Note: The examples in this article use Elixir 1.18.3. While most operations should work across different versions, some functionality might vary. Table of Contents Introduction Creating Anonymous Functions Function Capture Syntax Closures and Variable Capture Passing Anonymous Functions Common Patterns Best Practices Conclusion Further Reading Next Steps Introduction In previous articles, we've explored control flow structures and guards in Elixir. Now we'll dive into functions, starting with anonymous functions. Anonymous functions in Elixir are: First-class citizens, meaning they can be assigned to variables, passed as arguments, and returned from other functions Defined using the fn ... -> ... end syntax Called using the dot (.) operator Able to capture variables from their surrounding scope (closures) Often used for short, single-purpose operations Let's explore these powerful constructs that form the foundation of functional programming in Elixir. Creating Anonymous Functions Basic Syntax The basic syntax for creating an anonymous function in Elixir is: fn parameter1, parameter2, ... -> # Function body end Here's a simple example: iex> add = fn a, b -> a + b end #Function iex> add.(1, 2) 3 Notice that we call the function using the dot (.) operator, which distinguishes anonymous function calls from named function calls. Multiple Clauses Like named functions, anonymous functions can have multiple clauses with pattern matching: iex> calculator = fn {:add, a, b} -> a + b {:subtract, a, b} -> a - b {:multiply, a, b} -> a * b {:divide, a, b} when b != 0 -> a / b end iex> calculator.({:add, 5, 3}) 8 iex> calculator.({:subtract, 10, 4}) 6 iex> calculator.({:multiply, 2, 7}) 14 iex> calculator.({:divide, 10, 2}) 5.0 iex> calculator.({:divide, 10, 0}) ** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/1 This style is particularly useful when you need to handle different cases within a single function. Default Arguments Anonymous functions do not directly support default arguments like named functions. However, you can simulate this behavior with pattern matching, ensuring all clauses have the same arity: iex> greet = fn name, greeting \\ "Hello" -> "#{greeting}, #{name}!" end error: anonymous functions cannot have optional arguments └─ iex:1 ** (CompileError) cannot compile code (errors have been logged) # Simulating default arguments with pattern matching (using same arity) iex> greet = fn name, nil -> "Hello, #{name}!" name, greeting -> "#{greeting}, #{name}!" end iex> greet.("Alice", nil) "Hello, Alice!" iex> greet.("Bob", "Howdy") "Howdy, Bob!" Returning Functions Anonymous functions can return other anonymous functions, which is useful for creating function factories: iex> multiplier = fn factor -> fn number -> number * factor end end iex> double = multiplier.(2) #Function iex> triple = multiplier.(3) #Function iex> double.(5) 10 iex> triple.(5) 15 This example demonstrates important concepts in functional programming: Currying: Technically, it's the transformation of a function that receives multiple arguments into a sequence of functions, each accepting a single argument. In the example above, we're not really doing currying in the strict sense, since we start with separate functions. Partial Application: This is what we're actually doing here - providing only some of the arguments that a function expects, resulting in a new function that expects the remaining arguments. When we call multiplier.(2), we're creating a new function that has the value 2 "pre-applied" as the factor argument. The main difference is that currying is a specific transformation of multi-argument functions, while partial application is more general and refers to providing some arguments to a function and receiving another function in return. In pure functional languages like Haskell, all functions are automatically "curried", but in Elixir we need to implement these patterns explicitly. Function Capture Syntax Elixir provides a shorthand syntax for creating anonymous functions using the capture operator (&): iex> add = &(&1 + &2) &:erlang.+/2 # Elixir recognized that this expression maps to Erlang's addition function iex> add.(1, 2) 3 In the expression above, the output &:erlang.+/2 shows that Elixir optimized our captured function, recognizing it as a direct call to Erlang's native addition function. The /2 indicates the arity (number of ar

May 1, 2025 - 15:07
 0
Learning Elixir: Anonymous Functions

Anonymous functions are a cornerstone of functional programming in Elixir, allowing you to create functions on the fly without naming them. They're versatile building blocks that you can assign to variables, pass as arguments, or return from other functions. This article explores how to create and use anonymous functions effectively in your Elixir projects.

Note: The examples in this article use Elixir 1.18.3. While most operations should work across different versions, some functionality might vary.

Table of Contents

  • Introduction
  • Creating Anonymous Functions
  • Function Capture Syntax
  • Closures and Variable Capture
  • Passing Anonymous Functions
  • Common Patterns
  • Best Practices
  • Conclusion
  • Further Reading
  • Next Steps

Introduction

In previous articles, we've explored control flow structures and guards in Elixir. Now we'll dive into functions, starting with anonymous functions.

Anonymous functions in Elixir are:

  • First-class citizens, meaning they can be assigned to variables, passed as arguments, and returned from other functions
  • Defined using the fn ... -> ... end syntax
  • Called using the dot (.) operator
  • Able to capture variables from their surrounding scope (closures)
  • Often used for short, single-purpose operations

Let's explore these powerful constructs that form the foundation of functional programming in Elixir.

Creating Anonymous Functions

Basic Syntax

The basic syntax for creating an anonymous function in Elixir is:

fn parameter1, parameter2, ... ->
  # Function body
end

Here's a simple example:

iex> add = fn a, b -> a + b end
#Function<41.18682967/2 in :erl_eval.expr/6>

iex> add.(1, 2)
3

Notice that we call the function using the dot (.) operator, which distinguishes anonymous function calls from named function calls.

Multiple Clauses

Like named functions, anonymous functions can have multiple clauses with pattern matching:

iex> calculator = fn
  {:add, a, b} -> a + b
  {:subtract, a, b} -> a - b
  {:multiply, a, b} -> a * b
  {:divide, a, b} when b != 0 -> a / b
end

iex> calculator.({:add, 5, 3})
8

iex> calculator.({:subtract, 10, 4})
6

iex> calculator.({:multiply, 2, 7})
14

iex> calculator.({:divide, 10, 2})
5.0

iex> calculator.({:divide, 10, 0})
** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/1

This style is particularly useful when you need to handle different cases within a single function.

Default Arguments

Anonymous functions do not directly support default arguments like named functions. However, you can simulate this behavior with pattern matching, ensuring all clauses have the same arity:

iex> greet = fn
  name, greeting \\ "Hello" -> "#{greeting}, #{name}!"
end

error: anonymous functions cannot have optional arguments
└─ iex:1

** (CompileError) cannot compile code (errors have been logged)

# Simulating default arguments with pattern matching (using same arity)
iex> greet = fn
  name, nil -> "Hello, #{name}!"
  name, greeting -> "#{greeting}, #{name}!"
end

iex> greet.("Alice", nil)
"Hello, Alice!"

iex> greet.("Bob", "Howdy")
"Howdy, Bob!"

Returning Functions

Anonymous functions can return other anonymous functions, which is useful for creating function factories:

iex> multiplier = fn factor ->
  fn number -> number * factor end
end

iex> double = multiplier.(2)
#Function<43.3316493/1 in :erl_eval.expr/6>

iex> triple = multiplier.(3)
#Function<43.3316493/1 in :erl_eval.expr/6>

iex> double.(5)
10

iex> triple.(5)
15

This example demonstrates important concepts in functional programming:

  • Currying: Technically, it's the transformation of a function that receives multiple arguments into a sequence of functions, each accepting a single argument. In the example above, we're not really doing currying in the strict sense, since we start with separate functions.

  • Partial Application: This is what we're actually doing here - providing only some of the arguments that a function expects, resulting in a new function that expects the remaining arguments. When we call multiplier.(2), we're creating a new function that has the value 2 "pre-applied" as the factor argument.

The main difference is that currying is a specific transformation of multi-argument functions, while partial application is more general and refers to providing some arguments to a function and receiving another function in return. In pure functional languages like Haskell, all functions are automatically "curried", but in Elixir we need to implement these patterns explicitly.

Function Capture Syntax

Elixir provides a shorthand syntax for creating anonymous functions using the capture operator (&):

iex> add = &(&1 + &2)
&:erlang.+/2  # Elixir recognized that this expression maps to Erlang's addition function

iex> add.(1, 2)
3

In the expression above, the output &:erlang.+/2 shows that Elixir optimized our captured function, recognizing it as a direct call to Erlang's native addition function. The /2 indicates the arity (number of arguments) of the function. This optimization is common for arithmetic operators and other fundamental functions, resulting in more efficient runtime code.

In the capture syntax:

  • & begins the capture expression
  • &1, &2, etc. refer to the function's arguments by position
  • The entire expression is enclosed in parentheses

Capturing Named Functions

The capture operator can also be used to reference existing functions:

iex> string_length = &String.length/1
&String.length/1

iex> string_length.("hello")
5

iex> list_map = &Enum.map/2
&Enum.map/2

iex> list_map.([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]

This is especially useful when passing functions as arguments to other functions:

iex> Enum.map([1, 2, 3], &to_string/1)
["1", "2", "3"]

Shorthand Examples

The capture syntax allows for succinct anonymous functions:

# Traditional syntax
iex> double = fn x -> x * 2 end

# Capture syntax
iex> double = &(&1 * 2)

# Traditional syntax
iex> sum = fn a, b, c -> a + b + c end

# Capture syntax
iex> sum = &(&1 + &2 + &3)

# More complex example with a map
iex> extract_names = fn users -> Enum.map(users, &Map.get(&1, :name)) end

# Same with capture syntax
iex> extract_names = &Enum.map(&1, fn user -> Map.get(user, :name) end)

# Using two separate functions for better readability
iex> get_name = &Map.get(&1, :name)
iex> extract_names = &Enum.map(&1, get_name)

While the capture syntax is more concise, it can become harder to read with complex expressions. Use it judiciously, favoring readability over brevity.

Closures and Variable Capture

Anonymous functions in Elixir are closures, meaning they can access variables from the scope where they're defined:

iex> message = "Hello"
iex> greeter = fn name -> "#{message}, #{name}!" end
iex> greeter.("Alice")
"Hello, Alice!"

This is particularly useful when you need to preserve context:

iex> defmodule Counter do
  def create_counter(initial_value) do
    fn increment ->
      new_value = initial_value + increment
      {new_value, create_counter(new_value)}
    end
  end
end

iex> counter = Counter.create_counter(0)
iex> {value, counter} = counter.(1)
iex> value
1
iex> {value, counter} = counter.(2)
iex> value
3
iex> {value, _counter} = counter.(3)
iex> value
6

In this example, each time we call the counter function, it returns a new value and a new counter function with the updated value.

Variable Binding in Closures

Variables captured in closures are bound to their values at the time the function is created:

iex> multipliers = for n <- 1..5 do
  fn x -> n * x end
end

iex> Enum.map(multipliers, fn multiplier -> multiplier.(2) end)
[2, 4, 6, 8, 10]

Here, each function in the multipliers list captures a different value of n.

Avoiding Pitfalls with Closures

Be cautious when capturing references to mutable data structures. While Elixir emphasizes immutability, capturing process identifiers or references to external systems requires careful handling:

iex> defmodule CacheExample do
  def create_cached_lookup(initial_cache) do
    # This reference is shared across multiple calls
    cache_ref = :ets.new(:cache, [:set, :public])
    :ets.insert(cache_ref, initial_cache)

    fn key ->
      case :ets.lookup(cache_ref, key) do
        [{^key, value}] -> {:ok, value}
        [] -> {:error, :not_found}
      end
    end
  end
end

In this example, the anonymous function captures a reference to an ETS table, which is shared across all calls to the function.

Passing Anonymous Functions

One of the most common uses of anonymous functions is to pass them as arguments to other functions, particularly in collection operations:

iex> numbers = [1, 2, 3, 4, 5]

# Using an anonymous function directly
iex> Enum.map(numbers, fn x -> x * x end)
[1, 4, 9, 16, 25]

# Using the capture syntax
iex> Enum.filter(numbers, &(rem(&1, 2) == 0))
[2, 4]

# Combining operations
iex> numbers |> Enum.filter(&(&1 > 2)) |> Enum.map(&(&1 * 3)) |> Enum.sum()
36

Higher-Order Functions

Functions that accept or return other functions are called higher-order functions. Elixir's standard library includes many of these:

iex> defmodule HigherOrder do
  def apply_twice(fun, arg) do
    fun.(fun.(arg))
  end

  def compose(f, g) do
    fn x -> f.(g.(x)) end
  end
end

iex> double = fn x -> x * 2 end
iex> HigherOrder.apply_twice(double, 3)
12

iex> to_string = &Integer.to_string/1
iex> add_exclamation = fn s -> s <> "!" end
iex> exclaim_number = HigherOrder.compose(add_exclamation, to_string)
iex> exclaim_number.(42)
"42!"

Function References as Data

Anonymous functions can be stored in data structures, enabling powerful design patterns:

iex> operations = %{
  add: fn a, b -> a + b end,
  subtract: fn a, b -> a - b end,
  multiply: fn a, b -> a * b end,
  divide: fn a, b -> a / b end
}

iex> defmodule Calculator do
  def calculate(a, op, b, operations) do
    case Map.fetch(operations, op) do
      {:ok, operation} -> operation.(a, b)
      :error -> {:error, "Unknown operation: #{op}"}
    end
  end
end

iex> Calculator.calculate(10, :add, 5, operations)
15

iex> Calculator.calculate(10, :multiply, 5, operations)
50

iex> Calculator.calculate(10, :power, 2, operations)
{:error, "Unknown operation: power"}

Common Patterns

Let's explore some common patterns and idioms with anonymous functions in Elixir.

Transformation Pipelines

Anonymous functions shine when used in transformation pipelines:

iex> data = [%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, %{name: "Charlie", age: 35}]

iex> result = data |> Enum.filter(&(&1.age > 25)) |> Enum.map(&(&1.name)) |> Enum.join(", ")
"Alice, Charlie"

Implementing Callbacks

Anonymous functions are excellent for callback-style programming:

iex> defmodule TextProcessor do
  def process_text(content, processors) do
    result = content
    result = if processors[:pre_process], do: processors.pre_process.(result), else: result
    result = if processors[:transform], do: processors.transform.(result), else: result
    result = if processors[:post_process], do: processors.post_process.(result), else: result
    {:ok, result}
  end
end

iex> processors = %{
  pre_process: &String.trim/1,
  transform: &String.upcase/1,
  post_process: fn text -> "Processed: #{text}" end
}

iex> TextProcessor.process_text("  hello world  ", processors)
{:ok, "Processed: HELLO WORLD"}

# With just some processors
iex> TextProcessor.process_text("  hello world  ", %{transform: &String.upcase/1})
{:ok, "  HELLO WORLD  "}

Partial Application

Creating functions with some arguments already applied:

iex> defmodule Partial do
  def partially_apply(fun, args) do
    fn remaining_args ->
      apply(fun, args ++ [remaining_args])
    end
  end

  def partially_apply2(fun, args) do
    fn
      remaining_args when is_list(remaining_args) ->
        apply(fun, args ++ remaining_args);

      remaining_arg ->
        apply(fun, args ++ [remaining_arg])
    end
  end
end

iex> add = fn a, b, c -> a + b + c end
iex> add_to_5 = Partial.partially_apply(add, [2, 3])
iex> add_to_5.(4)
9

iex> greet = fn name, greeting -> "#{greeting}, #{name}!" end
iex> hello = Partial.partially_apply(greet, ["Hello"])
iex> hello.("Alice")
"Alice, Hello!"

The partially_apply2 function provides more flexibility by handling both single arguments and lists of arguments. For example:

iex> concat = fn a, b, c -> a <> b <> c end
iex> start_with_hello = Partial.partially_apply2(concat, ["Hello, "])
iex> start_with_hello.(["friend", "!"])
"Hello, friend!"
iex> start_with_hello.("world")
"Hello, world"  # This would fail with partially_apply

Best Practices

Here are some best practices to consider when working with anonymous functions in Elixir:

Keep Functions Small and Focused

Anonymous functions should generally be small and focused on a single responsibility:

# Good: Small, focused anonymous functions
iex> users = [
  %{age: 17, email: "young@example.com"},
  %{age: 21, email: "adult@example.com"},
  %{age: 35, email: "no-email"},
  %{age: 42, email: "complete@example.com"}
]
[
  %{age: 17, email: "young@example.com"},
  %{age: 21, email: "adult@example.com"},
  %{age: 35, email: "no-email"},
  %{age: 42, email: "complete@example.com"}
]
iex> adult? = &(&1.age >= 18)
#Function<42.18682967/1 in :erl_eval.expr/6>
iex> has_valid_email = &(String.contains?(&1.email, "@"))
#Function<42.18682967/1 in :erl_eval.expr/6>
iex> valid_users = users |> Enum.filter(adult?) |> Enum.filter(has_valid_email)
[%{age: 21, email: "adult@example.com"}, %{age: 42, email: "complete@example.com"}]

# Not as good: Doing too much in a single function
iex> valid_users = Enum.filter(users, fn user ->
  user.age >= 18 && String.contains?(user.email, "@")
end)

Balance Capture Syntax with Readability

While the capture syntax (&) is concise, prioritize readability:

# Less readable for complex operations
iex> transform = &(String.downcase(&1) |> String.replace(&2, &3) |> String.trim())

# More readable
iex> transform = fn string, pattern, replacement ->
  string
  |> String.downcase()
  |> String.replace(pattern, replacement)
  |> String.trim()
end

Name Functions When Possible

Even anonymous functions can benefit from meaningful variable names:

# Define a sample list and processing function
iex> data = ["apple", "banana", "cherry"]
["apple", "banana", "cherry"]
iex> process = fn str -> String.upcase(str) end
#Function<42.18682967/1 in :erl_eval.expr/6>

# Less clear - using capture syntax without naming
# Hard to tell at a glance what this code is trying to accomplish
iex> Enum.reduce(data, [], &[process.(&1) | &2])
["CHERRY", "BANANA", "APPLE"]

# More clear - named function communicates intent
# The name 'process_item' immediately tells us what the function does
iex> process_item = fn item -> process.(item) end
#Function<42.18682967/1 in :erl_eval.expr/6>
# The named parameters (item, acc) further enhance readability
iex> Enum.reduce(data, [], fn item, acc -> [process_item.(item) | acc] end)
["CHERRY", "BANANA", "APPLE"]

Use Closures Judiciously

Be careful with closures in long-lived applications, as they can capture references to resources:

# Potential issue: Closure capturing large data structure
iex> large_data = String.duplicate("x", 1_000_000)
iex> make_processor = fn ->
  # This closure captures 'large_data', which consumes 1MB of memory
  # Potential problems:
  # 1. Memory usage: Each instance of the function will maintain its own reference to the large structure
  # 2. Garbage collection: As long as the function exists, the captured data cannot be collected
  # 3. Inadvertent sharing: In long-running applications, this can lead to memory leaks
  fn input -> String.replace(input, "a", large_data) end
end

# Better approach: Pass large data as an argument
iex> make_processor = fn ->
  # This function doesn't capture any large structures
  # The large structure is passed explicitly when the function is called
  # Benefits:
  # 1. Clarity: Makes it explicit that large data is being used
  # 2. Efficiency: Allows proper GC and better memory management
  # 3. Flexibility: Allows using different large data for different calls
  fn input, replacement -> String.replace(input, "a", replacement) end
end
iex> processor = make_processor.()
iex> processor.("abc", large_data)

Consider Named Functions for Complex Logic

When anonymous functions become too complex, extract them to named functions:

# Complex anonymous function
iex> validator = fn data ->
  cond do
    !is_map(data) -> {:error, "Expected a map"}
    !Map.has_key?(data, :name) -> {:error, "Missing name field"}
    String.length(data.name) < 2 -> {:error, "Name too short"}
    !Map.has_key?(data, :age) -> {:error, "Missing age field"}
    !is_integer(data.age) -> {:error, "Age must be an integer"}
    data.age < 18 -> {:error, "Must be at least 18"}
    true -> {:ok, data}
  end
end

# Better as named functions in a module
iex> defmodule DataValidator do
  def validate(data) when not is_map(data), do: {:error, "Expected a map"}
  def validate(%{name: name, age: age}) do
    with {:ok} <- validate_name(name),
         {:ok} <- validate_age(age) do
      {:ok, %{name: name, age: age}}
    end
  end
  def validate(_), do: {:error, "Missing required fields"}

  defp validate_name(name) when is_binary(name) and byte_size(name) >= 2, do: {:ok}
  defp validate_name(_), do: {:error, "Invalid name"}

  defp validate_age(age) when is_integer(age) and age >= 18, do: {:ok}
  defp validate_age(_), do: {:error, "Invalid age"}
end

Conclusion

Anonymous functions are a fundamental building block in Elixir's functional programming paradigm. They allow you to:

  • Create functions on the fly and assign them to variables
  • Pass functions as arguments to higher-order functions
  • Return functions from other functions
  • Capture variables from the surrounding scope (closures)
  • Express transformations and callbacks succinctly

By understanding how to create, manipulate, and use anonymous functions effectively, you'll be able to leverage Elixir's functional nature to create more expressive, maintainable, and modular code.

Tip: Anonymous functions shine when used for short, single-purpose operations, especially in the context of collection manipulation. For complex or reusable logic, consider extracting them to named functions within modules.

Further Reading

Next Steps

In the upcoming article, we'll explore Named Functions:

Named Functions

  • Defining and organizing functions within modules
  • Public and private functions
  • Function arity and overloading
  • Default arguments and optional parameters
  • Documentation and typespecs for functions
  • Module attributes and function decorators

Named functions provide the structure and organization for your Elixir applications. While anonymous functions give you flexibility, named functions offer reusability, organization, and documentation capabilities. We'll explore how to define, document, and use them effectively to build maintainable Elixir codebases.