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

Control Flow

06-control-flow.livemd

Control Flow

if and unless

Its similar to JS

defmodule Temperature do
  def check_temp(temp) do
    if temp >= 30 do
      "It's hot!"
    else
      "It's not too hot."
    end
  end

  def check_freezing(temp) do
    unless temp > 0 do
      "It's freezing!"
    else
      "It's not freezing."
    end
  end
end
Temperature.check_freezing(35)
Temperature.check_temp(35)

In elixir everything returns a value, so we can assign the output from if/unless like this

character = "Yoda"

allegiance =
  if character in ["Yoda", "Luke", "Obi-Wan", "Rey"] do
    :jedi
  else
    if character in ["Vader", "Palpatine", "Maul", "Dooku"] do
      :sith
    else
      :unknown
    end
  end

IO.puts "#{character} is a #{allegiance}."    

In elixir variables in elixir are immutable within their specific scope

foo = 1
if true do
  foo = 2
end

Now we have foo outside if block and there is another one inside

when executed, the outer foo has no effect

foo

case

This is similar to a switch statement in other langs.

In JS it looks like this

const character = "Legolas";
let race;

switch (character) {
  case "Aragorn":
  case "Boromir":
    race = "Human";
    break;
  case "Legolas":
    race = "Elf";
    break;
  case "Gimli":
    race = "Dwarf";
    break;
  case "Frodo":
  case "Sam":
  case "Merry":
  case "Pippin":
    race = "Hobbit";
    break;
  case "Gandalf":
    race = "Wizard";
    break;
  default:
    race = "Unknown";
}

console.log(`${character} is a ${race}.`);
// Outputs: Legolas is an Elf.

In elixir

disaster_level_classifier = fn threat ->
  case threat do
    # Threat with minimal power and low destructiveness is a Wolf-level threat
    %{power: power, destructiveness: destructiveness} when power < 3 and destructiveness < 3 ->
      :wolf

    # Threat with moderate power or moderate destructiveness is a Tiger-level threat
    %{power: power, destructiveness: destructiveness}
    when (power >= 3 and power < 5) or (destructiveness >= 3 and destructiveness < 5) ->
      :tiger

    # Threat with high power and high destructiveness is a Demon-level threat
    %{power: power, destructiveness: destructiveness}
    when power >= 5 and destructiveness >= 5 and destructiveness < 8 ->
      :demon

    # Threat with very high power or destructiveness is a Dragon-level threat
    %{power: power, destructiveness: destructiveness} when power >= 8 or destructiveness >= 8 ->
      :dragon

    # A global catastrophe threat level with 
    # extreme power and destructiveness is classified as God-level
    %{power: power, destructiveness: destructiveness} when power >= 10 and destructiveness >= 10 ->
      :god

    # Fallback for unknown threats
    _ ->
      :unknown_threat
  end
end
IO.puts(disaster_level_classifier.(%{power: 2, destructiveness: 2}))        # Outputs: :wolf
IO.puts(disaster_level_classifier.(%{power: 4, destructiveness: 3}))        # Outputs: :tiger
IO.puts(disaster_level_classifier.(%{power: 6, destructiveness: 6}))        # Outputs: :demon
IO.puts(disaster_level_classifier.(%{power: 9, destructiveness: 8}))        # Outputs: :dragon
IO.puts(disaster_level_classifier.(%{power: 10, destructiveness: 10}))      # Outputs: :god
IO.puts(disaster_level_classifier.(%{power: 1, destructiveness: 10}))       # Outputs: :dragon
IO.puts(disaster_level_classifier.(%{unknown_key: :unknown_value}))         # Outputs: :unknown_threat

cond

cond is kind of similar to if/else-if/else statement. the difference between cond and case is in cond you can evaluate functions

defmodule WeatherAdvisor do
  def suggest_activity(weather, temperature) do
    cond do
      sunny?(weather) and temperature > 20 -> "It's a perfect day for a walk!"
      sunny?(weather) and temperature <= 20 -> "It's sunny but chilly. Bundle up for a walk!"
      rainy?(weather) -> "Take an umbrella!"
      snowy?(weather) and temperature <= 0 -> "It's freezing! Build a snowman!"
      true -> "Stay indoors and relax!"
    end
  end

  defp sunny?(weather), do: weather == :sunny
  defp rainy?(weather), do: weather == :rainy
  defp snowy?(weather), do: weather == :snowy
end
IO.puts WeatherAdvisor.suggest_activity(:sunny, 25)    
IO.puts WeatherAdvisor.suggest_activity(:sunny, 15)    
IO.puts WeatherAdvisor.suggest_activity(:rainy, 10)    
IO.puts WeatherAdvisor.suggest_activity(:snowy, -5)    
IO.puts WeatherAdvisor.suggest_activity(:cloudy, 15)   

Use of cond is bit rare btw

with

with is like a way to write the happy path without worrying too much about others.

It is kind of similar to early returns you see in other languages like javascript

Lets look at this Javascript example

function canAccessContent(user) {
  if (!user) {
    return "User not found";
  }
  
  if (!user.isLoggedIn) {
    return "User not logged in";
  }
  
  if (user.age < 18) {
    return "User is too young";
  }
  
  if (!user.isSubscribed) {
    return "User does not have a subscription";
  }
  
  return "Access granted";
}

We can write the same with elixir as follows

defmodule AccessControl do
  def can_access_content(user) do
    with {:ok, user} <- validate_user(user),
         {:ok, _} <- check_login(user),
         {:ok, _} <- check_age(user),
         {:ok, _} <- check_subscription(user) do
      "Access granted"
    else
      {:error, reason} -> reason
    end
  end

  defp validate_user(nil), do: {:error, "User not found"}
  defp validate_user(user), do: {:ok, user}

  defp check_login(%{is_logged_in: false}), do: {:error, "User not logged in"}
  defp check_login(%{is_logged_in: true}), do: {:ok, :logged_in}

  defp check_age(%{age: age}) when age < 18, do: {:error, "User is too young"}
  defp check_age(%{age: _}), do: {:ok, :age_valid}

  defp check_subscription(%{is_subscribed: false}),
    do: {:error, "User does not have a subscription"}

  defp check_subscription(%{is_subscribed: true}), do: {:ok, :subscribed}
end
IO.puts(AccessControl.can_access_content(nil))
IO.puts(AccessControl.can_access_content(%{is_logged_in: false}))
IO.puts(AccessControl.can_access_content(%{is_logged_in: true, age: 16}))
IO.puts(AccessControl.can_access_content(%{is_logged_in: true, age: 20, is_subscribed: false}))
IO.puts(AccessControl.can_access_content(%{is_logged_in: true, age: 20, is_subscribed: true}))

So as you can see we break it down to smaller functions, instead of having everything at one place.

This allows us to clearly see whats going on by just looking at the business logic defined in can_access_content/1

A real world example

This is a JS function which process some transaction data and store them, written with TypeScript

  async handleTransactionsTask(webhookResponse): Promise {
    this.logger.log(
      'Truelayer service transaction webhook response : %o',
      webhookResponse,
    );
    const { results_uri: transactionsDataUri, task_id: taskId } =
      webhookResponse;
    const taskResponse = await TruelayerTask.query()
      .where('taskId', taskId)
      .patch({
        status: webhookResponse.status,
      })
      .returning('*');

    const userId = taskResponse[0].userId;
    const credentialId = taskResponse[0].credentialId;

    const credentialData = await ObBankCredential.query()
      .where('id', credentialId)
      .select('accessToken')
      .first();
    const accessToken = credentialData.accessToken;

    const getTransactionsData = await this.truelayerClient.getDataFromUri(
      accessToken,
      transactionsDataUri,
    );

    if (getTransactionsData.results) {
      for (const transaction of getTransactionsData.results) {
        if (transaction.transaction_type == 'DEBIT') {
          const trxAmount = transaction.amount * 100;
          const amount = Math.floor(trxAmount);
          const transactionDataPayload: TruelayerTransactionType = {
            userId: userId,
            amount: amount,
            currencyCode: transaction.currency,
            merchantName: transaction.merchant_name,
            type: transaction.transaction_type,
            category: transaction.transaction_category,
            provider: Provider.TRUELAYER,
            providerTrxId: transaction.provider_transaction_id,
            description: transaction.description,
            status: TransactionStatus.Pending,
            trxTimestamp: new Date(transaction.timestamp),
          };

          this.logger.log(
            'Truelayer service transaction data payload : %o',
            transactionDataPayload,
          );

          await this.saveTruelayerTransactions(transactionDataPayload);
        }
      }
    }

    return;
  }

And same logic with elixir looks like follows

defmodule TransactionHandler do
  def handle_transactions_task(webhook_response) do
    log_webhook_response(webhook_response)

    with {:ok, task_response} <- update_task_status(webhook_response),
         {:ok, user_id} <- fetch_user_id(task_response),
         {:ok, credential_id} <- fetch_credential_id(task_response),
         {:ok, access_token} <- fetch_access_token(credential_id),
         {:ok, transactions_data} <- fetch_transactions_data(access_token, webhook_response.results_uri),
         {:ok, _} <- process_transactions(transactions_data, user_id) do
      :ok
    else
      {:error, reason} -> {:error, reason}
    end
  end

  defp log_webhook_response(webhook_response) do
    Logger.info("Truelayer service transaction webhook response: #{inspect(webhook_response)}")
  end

  defp update_task_status(%{task_id: task_id, status: status}) do
    # Outline of updating the task status in the database
    {:ok, task_response} = :stubbed_query
    {:ok, task_response}
  end

  defp fetch_user_id(task_response) do
    # Extract user_id from task response
    user_id = :stubbed_user_id
    {:ok, user_id}
  end

  defp fetch_credential_id(task_response) do
    # Extract credential_id from task response
    credential_id = :stubbed_credential_id
    {:ok, credential_id}
  end

  defp fetch_access_token(credential_id) do
    # Query to get access token by credential_id
    access_token = :stubbed_access_token
    {:ok, access_token}
  end

  defp fetch_transactions_data(access_token, transactions_data_uri) do
    # Fetch transaction data from TrueLayer using access token and data URI
    transactions_data = :stubbed_transactions_data
    {:ok, transactions_data}
  end

  defp process_transactions(transactions_data, user_id) do
    transactions = extract_transactions(transactions_data)

    Enum.each(transactions, fn transaction ->
      if transaction[:transaction_type] == :debit do
        transaction_data_payload = build_transaction_payload(transaction, user_id)
        Logger.info("Truelayer service transaction data payload: #{inspect(transaction_data_payload)}")
        save_transaction(transaction_data_payload)
      end
    end)

    {:ok, :transactions_processed}
  end

  defp extract_transactions(transactions_data) do
    # Extract and return transactions from the transactions data
    :stubbed_transactions
  end

  defp build_transaction_payload(transaction, user_id) do
    # Build and return the transaction data payload
    :stubbed_transaction_payload
  end

  defp save_transaction(transaction_data_payload) do
    # Save the transaction data payload
    :ok
  end
end