Learning Elixir: Named Functions

Named functions represent the foundational building blocks in Elixir's modular architecture, providing structure, organization, and reusability to your code. Unlike anonymous functions, named functions are defined within modules and can be referenced by their name, making them essential for building maintainable applications. This article explores the fundamentals of defining and using named functions in Elixir. 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 Defining Named Functions Function Clauses and Pattern Matching Public vs Private Functions Default Arguments Guards in Functions Documentation and Typespecs Module Attributes Best Practices Conclusion Further Reading Next Steps Introduction In previous articles, we explored advanced control structures and anonymous functions in Elixir. While anonymous functions provide flexibility and convenience for one-off operations, named functions form the building blocks of your application's architecture. Named functions in Elixir are: Defined within modules using the def or defp keywords Identified by their name and arity (number of arguments) Callable from other modules when defined as public Able to leverage pattern matching across multiple function clauses Documentable with standardized comment syntax Specifiable with type information for better tooling support Let's explore how to define, organize, and use named functions effectively in your Elixir projects. Defining Named Functions Basic Syntax The basic syntax for defining a named function in Elixir is: defmodule ModuleName do def function_name(parameter1, parameter2, ...) do # Function body end end Here's a simple example: defmodule Calculator do def add(a, b) do a + b end end You can then call this function with: iex> Calculator.add(5, 3) 8 Single-Line Function Syntax For short functions, you can use the single-line syntax: defmodule Calculator do def add(a, b), do: a + b def subtract(a, b), do: a - b def multiply(a, b), do: a * b def divide(a, b), do: a / b end This is functionally equivalent to the multi-line syntax but more concise for simple functions. Calling Functions Named functions are called using the module name followed by the function name: iex> Calculator.add(1, 2) 3 iex> Calculator.multiply(4, 5) 20 Function Return Values In Elixir, functions always return the value of the last expression evaluated: defmodule Example do def return_last_value do 1 2 "This will be returned" end def conditional_return(x) do if x > 10 do "Greater than 10" else "10 or less" end end end Testing in IEx: iex> Example.return_last_value() "This will be returned" iex> Example.conditional_return(15) "Greater than 10" iex> Example.conditional_return(5) "10 or less" Function Naming Conventions Elixir has established naming conventions that make code more readable and predictable: defmodule StringHelper do # Predicate function (returns boolean) ends with ? def valid?(string) do is_binary(string) and String.length(string) > 0 end # Function that raises an exception ends with ! def tokenize!(string) do if valid?(string) do String.split(string, " ") else raise ArgumentError, "Invalid string provided" end end # Safe version without ! doesn't raise exceptions def tokenize(string) do if valid?(string) do {:ok, String.split(string, " ")} else {:error, "Invalid string provided"} end end end Testing in IEx: iex> StringHelper.valid?("hello world") true iex> StringHelper.valid?("") false iex> StringHelper.tokenize!("hello world") ["hello", "world"] iex> StringHelper.tokenize!("") ** (ArgumentError) Invalid string provided iex> StringHelper.tokenize("hello world") {:ok, ["hello", "world"]} iex> StringHelper.tokenize("") {:error, "Invalid string provided"} Function names in Elixir: Must start with a lowercase letter (a-z) or underscore (_) Can contain alphanumeric characters, underscores, and the special ending characters ? and ! Conventionally use snake_case (lowercase with underscores) Cannot contain other special characters or operators Common conventions to follow: Functions returning booleans should end with ? (e.g., empty?, valid?, exists?) Functions that raise exceptions should end with ! (e.g., save!, fetch!, decode!) Provide both safe (returns tuple) and unsafe (raises exception) versions of important functions Use descriptive verb-noun combinations (e.g., parse_json, validate_user, calculate_total) Function Arity In Elixir, functions are identified by their name and arity (the number of arguments they accept). The combination of name and arity is often

May 10, 2025 - 13:55
 0
Learning Elixir: Named Functions

Named functions represent the foundational building blocks in Elixir's modular architecture, providing structure, organization, and reusability to your code. Unlike anonymous functions, named functions are defined within modules and can be referenced by their name, making them essential for building maintainable applications. This article explores the fundamentals of defining and using named functions in Elixir.

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
  • Defining Named Functions
  • Function Clauses and Pattern Matching
  • Public vs Private Functions
  • Default Arguments
  • Guards in Functions
  • Documentation and Typespecs
  • Module Attributes
  • Best Practices
  • Conclusion
  • Further Reading
  • Next Steps

Introduction

In previous articles, we explored advanced control structures and anonymous functions in Elixir. While anonymous functions provide flexibility and convenience for one-off operations, named functions form the building blocks of your application's architecture.

Named functions in Elixir are:

  • Defined within modules using the def or defp keywords
  • Identified by their name and arity (number of arguments)
  • Callable from other modules when defined as public
  • Able to leverage pattern matching across multiple function clauses
  • Documentable with standardized comment syntax
  • Specifiable with type information for better tooling support

Let's explore how to define, organize, and use named functions effectively in your Elixir projects.

Defining Named Functions

Basic Syntax

The basic syntax for defining a named function in Elixir is:

defmodule ModuleName do
  def function_name(parameter1, parameter2, ...) do
    # Function body
  end
end

Here's a simple example:

defmodule Calculator do
  def add(a, b) do
    a + b
  end
end

You can then call this function with:

iex> Calculator.add(5, 3)
8

Single-Line Function Syntax

For short functions, you can use the single-line syntax:

defmodule Calculator do
  def add(a, b), do: a + b
  def subtract(a, b), do: a - b
  def multiply(a, b), do: a * b
  def divide(a, b), do: a / b
end

This is functionally equivalent to the multi-line syntax but more concise for simple functions.

Calling Functions

Named functions are called using the module name followed by the function name:

iex> Calculator.add(1, 2)
3

iex> Calculator.multiply(4, 5)
20

Function Return Values

In Elixir, functions always return the value of the last expression evaluated:

defmodule Example do
  def return_last_value do
    1
    2
    "This will be returned"
  end

  def conditional_return(x) do
    if x > 10 do
      "Greater than 10"
    else
      "10 or less"
    end
  end
end

Testing in IEx:

iex> Example.return_last_value()
"This will be returned"

iex> Example.conditional_return(15)
"Greater than 10"

iex> Example.conditional_return(5)
"10 or less"

Function Naming Conventions

Elixir has established naming conventions that make code more readable and predictable:

defmodule StringHelper do
  # Predicate function (returns boolean) ends with ?
  def valid?(string) do
    is_binary(string) and String.length(string) > 0
  end

  # Function that raises an exception ends with !
  def tokenize!(string) do
    if valid?(string) do
      String.split(string, " ")
    else
      raise ArgumentError, "Invalid string provided"
    end
  end

  # Safe version without ! doesn't raise exceptions
  def tokenize(string) do
    if valid?(string) do
      {:ok, String.split(string, " ")}
    else
      {:error, "Invalid string provided"}
    end
  end
end

Testing in IEx:

iex> StringHelper.valid?("hello world")
true

iex> StringHelper.valid?("")
false

iex> StringHelper.tokenize!("hello world")
["hello", "world"]

iex> StringHelper.tokenize!("")
** (ArgumentError) Invalid string provided

iex> StringHelper.tokenize("hello world")
{:ok, ["hello", "world"]}

iex> StringHelper.tokenize("")
{:error, "Invalid string provided"}

Function names in Elixir:

  • Must start with a lowercase letter (a-z) or underscore (_)
  • Can contain alphanumeric characters, underscores, and the special ending characters ? and !
  • Conventionally use snake_case (lowercase with underscores)
  • Cannot contain other special characters or operators

Common conventions to follow:

  • Functions returning booleans should end with ? (e.g., empty?, valid?, exists?)
  • Functions that raise exceptions should end with ! (e.g., save!, fetch!, decode!)
  • Provide both safe (returns tuple) and unsafe (raises exception) versions of important functions
  • Use descriptive verb-noun combinations (e.g., parse_json, validate_user, calculate_total)

Function Arity

In Elixir, functions are identified by their name and arity (the number of arguments they accept). The combination of name and arity is often written as name/arity.

defmodule StringHandler do
  # These are different functions, even though they share the same name
  def process(string), do: String.upcase(string)  # process/1
  def process(string, options), do: String.upcase(string) <> options[:suffix]  # process/2
end

Testing in IEx:

iex> StringHandler.process("hello")
"HELLO"

iex> StringHandler.process("hello", suffix: "!")
"HELLO!"

Functions with the same name but different arity are completely separate functions. When referring to a specific function, it's common to use the name/arity notation, like String.length/1 or Enum.map/2.

This naming convention is important when:

  • Documenting functions
  • Importing or referring to functions from other modules
  • Using the function capture syntax (e.g., &String.upcase/1)

Function Clauses and Pattern Matching

One of Elixir's most powerful features is the ability to define multiple function clauses with pattern matching:

defmodule Geometry do
  def area({:rectangle, width, height}) do
    width * height
  end

  def area({:circle, radius}) do
    :math.pi() * radius * radius
  end

  def area({:triangle, base, height}) do
    (base * height) / 2
  end
end

Testing in IEx:

iex> Geometry.area({:rectangle, 4, 5})
20

iex> Geometry.area({:circle, 3})
28.274333882308138

iex> Geometry.area({:triangle, 6, 8})
24.0

The appropriate function clause is selected based on the pattern of the arguments.

Function Clauses Order

The order of function clauses matters, as they are tried from top to bottom:

defmodule ListProcessor do
  def count([]), do: 0
  def count([_head | tail]), do: 1 + count(tail)
end

Testing in IEx:

iex> ListProcessor.count([])
0

iex> ListProcessor.count([1, 2, 3, 4])
4

If we were to reverse the order of these function clauses, the empty list case would never be reached, resulting in an infinite recursion.

Public vs Private Functions

Elixir provides two keywords for defining functions:

  • def for public functions that can be called from outside the module
  • defp for private functions that can only be called from within the module
defmodule User do
  def register(name, email, password) do
    # Call private functions to process the registration
    with {:ok, validated_name} <- validate_name(name),
         {:ok, validated_email} <- validate_email(email),
         {:ok, hashed_password} <- hash_password(password) do
      {:ok, %{name: validated_name, email: validated_email, password_hash: hashed_password}}
    else
      {:error, reason} -> {:error, reason}
    end
  end

  defp validate_name(name) when is_binary(name) and byte_size(name) > 0 do
    {:ok, String.trim(name)}
  end
  defp validate_name(_), do: {:error, "Invalid name"}

  defp validate_email(email) when is_binary(email) do
    if String.contains?(email, "@") do
      {:ok, String.downcase(email)}
    else
      {:error, "Invalid email format"}
    end
  end
  defp validate_email(_), do: {:error, "Invalid email"}

  defp hash_password(password) when is_binary(password) and byte_size(password) >= 8 do
    # In a real application, you would use a proper password hashing library
    {:ok, :crypto.hash(:sha256, password) |> Base.encode16()}
  end
  defp hash_password(_), do: {:error, "Password too short"}
end

Testing in IEx:

iex> User.register("Alice", "alice@example.com", "password123")
{:ok, %{
  name: "Alice",
  email: "alice@example.com",
  password_hash: "CBFDAC6008F9CAB4083784CBD1874F76618D2A97316EB8D2BFE65DC35D37D6AB"
}}

iex> User.register("", "invalid-email", "short")
{:error, "Invalid name"}

iex> User.validate_name("Alice")
** (UndefinedFunctionError) function User.validate_name/1 is undefined or private

Private functions (defp) are only accessible from within the module, promoting encapsulation and ensuring that implementation details remain hidden.

Default Arguments

Elixir supports default arguments for functions, which are used when the caller doesn't provide a value:

defmodule Greeter do
  def hello(name, language \\ "en") do
    phrase = case language do
      "en" -> "Hello"
      "es" -> "Hola"
      "fr" -> "Bonjour"
      "pt" -> "Olá"
      _ -> "Hello"
    end

    "#{phrase}, #{name}!"
  end
end

Testing in IEx:

iex> Greeter.hello("Alice")
"Hello, Alice!"

iex> Greeter.hello("Carlos", "es")
"Hola, Carlos!"

iex> Greeter.hello("Pierre", "fr")
"Bonjour, Pierre!"

Multiple Function Clauses with Default Arguments

When using default arguments with multiple function clauses, you need to be careful:

defmodule Messenger do
  def send_message(message, options \\ [])

  def send_message(message, options) when is_binary(message) do
    recipient = Keyword.get(options, :recipient, "everyone")
    priority = Keyword.get(options, :priority, "normal")

    "Sending '#{message}' to #{recipient} with #{priority} priority"
  end

  def send_message(messages, options) when is_list(messages) do
    Enum.map(messages, &send_message(&1, options))
  end
end

When using default arguments with multiple clauses, you need to provide a function head (a function declaration without a body) that specifies the defaults.

Testing in IEx:

iex> Messenger.send_message("Hello")
"Sending 'Hello' to everyone with normal priority"

iex> Messenger.send_message("Urgent update", recipient: "team", priority: "high")
"Sending 'Urgent update' to team with high priority"

iex> Messenger.send_message(["Hello", "How are you?"])
["Sending 'Hello' to everyone with normal priority", "Sending 'How are you?' to everyone with normal priority"]

Guards in Functions

Guards allow you to extend pattern matching with extra conditions:

defmodule NumberClassifier do
  def classify(x) when is_integer(x) and x < 0 do
    "negative integer"
  end

  def classify(x) when is_integer(x) and x == 0 do
    "zero"
  end

  def classify(x) when is_integer(x) and x > 0 do
    "positive integer"
  end

  def classify(x) when is_float(x) and x < 0 do
    "negative float"
  end

  def classify(x) when is_float(x) and x == 0.0 do
    "zero as float"
  end

  def classify(x) when is_float(x) and x > 0 do
    "positive float"
  end

  def classify(_) do
    "not a number"
  end
end

Testing in IEx:

iex> NumberClassifier.classify(-10)
"negative integer"

iex> NumberClassifier.classify(0)
"zero"

iex> NumberClassifier.classify(42)
"positive integer"

iex> NumberClassifier.classify(-3.14)
"negative float"

iex> NumberClassifier.classify(0.0)
"zero as float"

iex> NumberClassifier.classify(2.71)
"positive float"

iex> NumberClassifier.classify("hello")
"not a number"

Guards are limited to a specific set of operations and functions, mainly those that don't have side effects and execute quickly.

Documentation and Typespecs

Good documentation is essential for maintainable code. Elixir provides built-in tools for documenting modules and functions:

defmodule StringUtil do
  @moduledoc """
  Utility functions for string manipulation.

  This module provides a collection of functions for common string
  operations not covered by the standard String module.
  """

  @doc """
  Truncates a string to the specified length and adds an ellipsis.

  ## Parameters
    * `string` - The string to truncate
    * `length` - The maximum length before truncation
    * `ellipsis` - The string to append after truncation (default: "...")

  ## Examples
      iex> StringUtil.truncate("Hello, world!", 5)
      "Hello..."

      iex> StringUtil.truncate("Short", 10)
      "Short"

      iex> StringUtil.truncate("Hello, world!", 5, " [more]")
      "Hello [more]"
  """
  @spec truncate(String.t(), non_neg_integer(), String.t()) :: String.t()
  def truncate(string, length, ellipsis \\ "...") when is_binary(string) do
    if String.length(string) > length do
      String.slice(string, 0, length) <> ellipsis
    else
      string
    end
  end

  @doc """
  Extracts all hashtags from a string.

  Returns a list of hashtags (without the # symbol) found in the string.

  ## Examples
      iex> StringUtil.extract_hashtags("Hello #world from #elixir")
      ["world", "elixir"]

      iex> StringUtil.extract_hashtags("No hashtags here")
      []
  """
  @spec extract_hashtags(String.t()) :: [String.t()]
  def extract_hashtags(string) when is_binary(string) do
    Regex.scan(~r/#(\w+)/, string, capture: :all_but_first)
    |> List.flatten()
  end
end

The module includes:

  • @moduledoc for module-level documentation
  • @doc for function-level documentation
  • @spec for type specifications

These annotations not only provide documentation for developers but also enable tooling like ExDoc to generate HTML documentation and dialyzer for static type checking.

Module Attributes

Module attributes in Elixir serve multiple purposes:

  1. Configuration and constants
  2. Documentation (as we've seen)
  3. Temporary storage during compilation

Constants and Configuration

defmodule AppConfig do
  @timeout 5000
  @default_pool_size 10
  @supported_formats [:json, :xml, :csv]

  def timeout, do: @timeout
  def default_pool_size, do: @default_pool_size
  def supported_formats, do: @supported_formats

  def format_supported?(format) do
    format in @supported_formats
  end
end

Testing in IEx:

iex> AppConfig.timeout()
5000

iex> AppConfig.format_supported?(:json)
true

iex> AppConfig.format_supported?(:yaml)
false

Compile-Time Computation

Module attributes can be used for compile-time computations:

defmodule MathConstants do
  # These values are calculated once when the module is compiled
  @pi :math.pi()
  @e :math.exp(1)
  @golden_ratio (1 + :math.sqrt(5)) / 2

  # A more complex example - calculating prime numbers at compile time
  # We wrap the calculation in a function that executes immediately
  @primes (fn ->
    # Special case for 2 (the only even prime number)
    # We combine it with other primes found through filtering
    [2 | Enum.filter(3..30, fn n ->
      # A number is prime if it's only divisible by 1 and itself
      # We only need to check divisors up to the square root of n
      limit = :math.sqrt(n) |> trunc()
      # If n is divisible by any number from 2 to sqrt(n), it's not prime
      Enum.all?(2..limit, fn i -> rem(n, i) != 0 end)
    end)]
  end).()  # The () at the end executes the function during compilation

  # These functions just return the pre-computed values
  def pi, do: @pi
  def e, do: @e
  def golden_ratio, do: @golden_ratio
  def primes, do: @primes
end

Testing in IEx:

iex> MathConstants.pi()
3.141592653589793

iex> MathConstants.e()
2.718281828459045

iex> MathConstants.golden_ratio()
1.618033988749895

iex> MathConstants.primes()
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

The prime numbers computation happens at compile time, not at runtime, improving performance.

Best Practices

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

Keep Functions Small and Focused

Each function should have a single responsibility:

# Not so good: one function doing multiple things
def process_user_data(user) do
  # Validate
  unless is_map(user) and Map.has_key?(user, :name) do
    raise "Invalid user data"
  end

  # Transform
  user = Map.update(user, :name, "", &String.upcase/1)

  # Persist
  save_to_database(user)

  # Return
  {:ok, user}
end

# Better: Split into focused functions
def process_user_data(user) do
  with {:ok, valid_user} <- validate_user(user),
       {:ok, processed_user} <- transform_user(valid_user),
       {:ok, saved_user} <- persist_user(processed_user) do
    {:ok, saved_user}
  end
end

defp validate_user(user) when is_map(user) and is_binary(user[:name]) do
  {:ok, user}
end
defp validate_user(_), do: {:error, "Invalid user data"}

defp transform_user(user) do
  {:ok, Map.update(user, :name, "", &String.upcase/1)}
end

defp persist_user(user) do
  # Logic to save to database
  {:ok, user}
end

Use Pattern Matching in Function Definitions

Leverage pattern matching for clearer and more concise code:

# Less clear approach using conditionals
def process_list(list) do
  if length(list) == 0 do
    :empty
  else
    :non_empty
  end
end

# Better approach using pattern matching
def process_list([]), do: :empty
def process_list(_non_empty_list), do: :non_empty

Use Function Clauses for Different Cases

Split complex logic into multiple function clauses:

defmodule PaymentProcessor do
  def process_payment(%{type: "credit_card"} = payment) do
    # Credit card specific processing
    {:ok, charge_credit_card(payment)}
  end

  def process_payment(%{type: "bank_transfer"} = payment) do
    # Bank transfer specific processing
    {:ok, process_bank_transfer(payment)}
  end

  def process_payment(%{type: "digital_wallet"} = payment) do
    # Digital wallet specific processing
    {:ok, process_digital_wallet(payment)}
  end

  def process_payment(_), do: {:error, "Unsupported payment type"}

  # Private implementation functions
  defp charge_credit_card(payment), do: %{id: generate_id(), status: "charged", payment: payment}
  defp process_bank_transfer(payment), do: %{id: generate_id(), status: "pending", payment: payment}
  defp process_digital_wallet(payment), do: %{id: generate_id(), status: "completed", payment: payment}

  defp generate_id, do: "tx_#{:rand.uniform(1000)}"
end

Use Consistent Return Values

Adopt a consistent pattern for function returns to make error handling easier:

# Consistent return values using {:ok, result} and {:error, reason}
defmodule UserRepository do
  def find_by_id(id) when is_integer(id) and id > 0 do
    # Simulate database lookup
    if id == 42 do
      {:ok, %{id: 42, name: "Alice", email: "alice@example.com"}}
    else
      {:error, "User not found"}
    end
  end

  def find_by_id(_), do: {:error, "Invalid ID"}

  def create(user) when is_map(user) do
    # Validation and creation logic
    if Map.has_key?(user, :name) and Map.has_key?(user, :email) do
      {:ok, Map.put(user, :id, :rand.uniform(1000))}
    else
      {:error, "Invalid user data"}
    end
  end

  def create(_), do: {:error, "Invalid user data"}

  def update(id, changes) when is_integer(id) and is_map(changes) do
    # Update logic
    with {:ok, user} <- find_by_id(id) do
      {:ok, Map.merge(user, changes)}
    end
  end

  def update(_, _), do: {:error, "Invalid update parameters"}
end

Document Your Functions

Add appropriate documentation for clarity and maintainability:

defmodule EmailValidator do
  @moduledoc """
  Provides functions for validating email addresses.
  """

  @doc """
  Validates an email address format.

  Returns `:ok` if the email is valid, or `{:error, reason}` if invalid.

  ## Examples

      iex> EmailValidator.validate("user@example.com")
      :ok

      iex> EmailValidator.validate("invalid-email")
      {:error, "Invalid email format"}
  """
  @spec validate(String.t()) :: :ok | {:error, String.t()}
  def validate(email) when is_binary(email) do
    if Regex.match?(~r/^[^\s]+@[^\s]+\.[^\s]+$/, email) do
      :ok
    else
      {:error, "Invalid email format"}
    end
  end

  def validate(_), do: {:error, "Email must be a string"}
end

Organize Related Functions in Coherent Modules

Group related functions into modules that represent a clear domain concept:

# Too broad - mixing concerns
defmodule Utils do
  def format_date(date), do: # ...
  def validate_email(email), do: # ...
  def calculate_tax(amount), do: # ...
  def generate_random_string(length), do: # ...
end

# Better organization
defmodule DateFormatter do
  def format(date, format \\ :iso), do: # ...
  defp parse_format(format), do: # ...
end

defmodule Validators do
  def email(email), do: # ...
  def password(password), do: # ...
end

defmodule TaxCalculator do
  def calculate(amount, rate), do: # ...
  def apply_exemptions(amount, exemptions), do: # ...
end

defmodule StringGenerator do
  def random(length, type \\ :alphanumeric), do: # ...
  defp character_set(type), do: # ...
end

Conclusion

Named functions are the foundation of Elixir programming, providing structure, organization, and reusability to your code. By leveraging pattern matching, guards, documentation, and module organization, you can create expressive and maintainable codebases.

Key takeaways from this article include:

  • Named functions are defined within modules using the def and defp keywords
  • Pattern matching in function clauses enables expressive and clean code
  • Public functions (def) can be called from anywhere, while private functions (defp) are module-internal
  • Default arguments provide flexibility and convenience
  • Guards extend pattern matching with additional conditions
  • Documentation and typespecs improve clarity and enable tooling
  • Module attributes provide compile-time constants and configuration

By mastering named functions, you'll be able to design robust and maintainable Elixir applications that leverage the language's functional paradigm effectively.

Tip: When deciding between named and anonymous functions, use named functions for reusable logic and core application functionality, and anonymous functions for short, one-off operations, especially in the context of collection processing.

Further Reading

Next Steps

In the upcoming article, we'll explore Pattern Matching in Functions:

Pattern Matching in Functions

  • Advanced pattern matching techniques in function parameters
  • Multi-clause functions for different input patterns
  • Destructuring complex data structures in function arguments
  • Using guards to extend pattern matching capabilities
  • Common patterns and idioms using pattern matching in functions
  • Best practices for readable and maintainable pattern-matched functions

Pattern matching is a fundamental concept in Elixir that we've already touched on, but the next article will dive deeper into its application specifically in function definitions. You'll learn how to leverage pattern matching to create elegant, expressive, and robust function implementations.