Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Functions and Modules

05-functions-and-modules.livemd

Functions and Modules

Defining functions

In elixir we can define functions with fn keyword

add = fn a, b -> a + b end

This is an anonymous function

In Javascript we write this like

const adder = (a, b) => a + b

in python this is known as a lambda function

adder = lambda a, b: a + b

Now lets execute our add function

add.(10, 11)

to invoke an anonymous function you need to use a dot(.)

this is used to identify anon function invoking from other function definitions

Closures

Like in JS we can access surrounding scope

# Create an anonymous function that takes a greeting and returns another function
make_greeting = fn greeting ->
  # This inner function captures the `greeting` variable, creating a closure
  fn name -> 
    "#{greeting}, #{name}!"
  end
end

# Create a closure that says "Hello"
hello = make_greeting.("Hello")

# Create another closure that says "Goodbye"
goodbye = make_greeting.("Goodbye")
hello.("WaveZync ๐ŸŒŠ")
goodbye.("World")

In JS it looks like this

// Create a function that takes a greeting and returns another function
const makeGreeting = (greeting) => (name) => `${greeting}, ${name}!`;

// Create a closure that says "Hello"
const hello = makeGreeting("Hello");

// Create another closure that says "Goodbye"
const goodbye = makeGreeting("Goodbye");

Clauses and Guards

In the following function we have two parameters

in each clause we are doing pattern matching and same time using a guard with when

as a fallback we have the last catch all clause to handle any other param

can_drink = fn
  age, "USA" when age >= 21 -> "You're allowed to drink in the USA."
  _age, "USA" -> "You're not allowed to drink in the USA."
  
  age, "Germany" when age >= 16 -> "You're allowed to drink in Germany."
  _age, "Germany" -> "You're not allowed to drink in Germany."

  age, "Japan" when age >= 20 -> "You're allowed to drink in Japan."
  _age, "Japan" -> "You're not allowed to drink in Japan."

  _, country -> "Drinking age information for #{country} is not available."
end
IO.puts can_drink.(18, "USA")       
IO.puts can_drink.(21, "USA")       
IO.puts can_drink.(17, "Germany")   
IO.puts can_drink.(15, "Germany")   
IO.puts can_drink.(20, "Japan")     
IO.puts can_drink.(18, "Japan")     
IO.puts can_drink.(18, "France")    

Capture Operator

We used func_name/arity convention(like put_in/2), actually it can be used with capture operator

add = &+/2
add.(1,3)

Remember in Elixir + is also a function ๐Ÿ˜Š (Defined in Kernel module as Kernel.+/2)

This becomese useful later with data transformation

Now we can capture any function as a variable also

We can also use a shorthand notation for anon functions

add_1 = &(&1 + &2)
add_2 = fn a, b -> a + b end
add_1.(1, 5)
add_2.(1, 5)

This is used practically to simplify many data transformations an example is listed below

names = ["alice", nil, "bob", "", "carol"]

result =
  names
  |> Enum.reject(&is_nil/1)                    # Remove nil values
  |> Enum.reject(&(&1 == ""))                  # Remove empty strings
  |> Enum.map(&String.upcase/1)                # Convert each name to uppercase
  |> Enum.map(&("Name: #{&1}"))                # Format each name with a prefix

IO.inspect(result)

Modules

Modules allow to organize functions. In other languages this is like a class(but there is a difference)

Simply think of this as a group of functions

defmodule MathUtils do
  @moduledoc """
  A simple module with basic mathematical utility functions.
  """

  @doc """
  Adds two numbers.
  """
  def add(a, b) when is_number(a) and is_number(b) do
    a + b
  end

  @doc """
  Multiplies two numbers.
  """
  def multiply(a, b) when is_number(a) and is_number(b) do
    a * b
  end

  @doc """
  Checks if a number is even.
  """
  def even?(n) when is_number(n) do
    rem(n, 2) == 0
  end

  @doc """
  Calculates the square of a number.
  """
  def square(n) when is_number(n) do
    n * n
  end

  @doc """
  Describes a number as positive, negative, or zero.
  """
  def describe_number(n) when is_number(n) and n > 0 do
    "The number is positive."
  end

  def describe_number(n) when is_number(n) and n < 0 do
    "The number is negative."
  end

  def describe_number(0) do
    "The number is zero."
  end

  def describe_number(_non_number) do
    "This is not a valid number."
  end
end
# Testing the module
IO.puts MathUtils.describe_number(5)       # Outputs: The number is positive.
IO.puts MathUtils.describe_number(-3)      # Outputs: The number is negative.
IO.puts MathUtils.describe_number(0)       # Outputs: The number is zero.
IO.puts MathUtils.describe_number("hello") # Outputs: This is not a valid number.

In elixir as a convention we are using ? to indicate a function result will be a boolean

In above example we can see describe_number/1 appear multiple times with guards

This pattern allows us to move logic into function signature and do pattern matching to get a result Its a common pattern in elixir to pattern match on function signature itself

Also compared to a if statement, when you have patterns in the function header compiler does optimizations

Private functions

We can define private functions with defp.

In elixir its common to have lot of small private functions

defmodule Greeter do
  # Public function that uses a private helper function
  def greet(name) do
    message = format_greeting(name)
    "Greeting message: #{message}"
  end

  # Private function to format the greeting
  defp format_greeting(name) do
    "Hello, #{name}!"
  end
end
Greeter.greet("Kasun")

Default Parameters

We can pass default params with \\

defmodule Greeting do
  def greet(name, greeting \\ "Hello") do
    "#{greeting}, #{name}!"
  end
end
IO.puts Greeting.greet("Alice")             # Outputs: Hello, Alice!
IO.puts Greeting.greet("Alice", "Hi")       # Outputs: Hi, Alice!
IO.puts Greeting.greet("Alice", "Welcome")  # Outputs

If a function with default values has multiple clauses, we need to createe a function head (definition without body) to declarae defaults

defmodule Describe do
  # Function head declaring defaults
  def details(name, age \\ nil, title \\ "Unknown")

  # Clause where age is not provided
  def details(name, nil, title) do
    "#{title} #{name}"
  end

  # Clause where all parameters are provided
  def details(name, age, title) do
    "#{title} #{name}, age #{age}"
  end
end
IO.puts Describe.details("Alice", 30, "Dr.")      # Outputs: Dr. Alice, age 30
IO.puts Describe.details("Alice", 30)             # Outputs: Unknown Alice, age 30
IO.puts Describe.details("Alice")                 # Outputs: Unknown Alice