Skip to content

Latest commit

 

History

History
296 lines (227 loc) · 7.7 KB

with.livemd

File metadata and controls

296 lines (227 loc) · 7.7 KB

With

Mix.install([
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"},
  {:tested_cell, github: "brooklinjazz/tested_cell"},
  {:utils, path: "#{__DIR__}/../utils"}
])

Navigation

Return Home Report An Issue

Setup

Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.

With

with is often used with pattern matching to create "happy path" code. It's useful whenever you have a series of cases or values that rely on each other.

You can use with to check some preconditions before executing instructions.

flowchart LR
  with --> 1
  1 --> 2
  2 --> 3
  3 --> 4
  1[pre-condition]
  2[pre-condition]
  3[pre-condition]
  4[instruction]
Loading

If any of the preconditions fail, the with statement will stop and return the value of the failed precondition.

flowchart LR
  1[pre-condition]
  2[pre-condition]
  3[pre-condition]
  4[instruction]
  with --> 1
  1 --> 2
  2 --> 3
  3 --> 4
  1 --> 5[failed pre-condition]
  2 --> 5
  3 --> 5
Loading

Alternatively, you can use else to handle the result of a failed precondition.

flowchart LR
  1[pre-condition]
  2[pre-condition]
  3[pre-condition]
  4[instruction]
  with --> 1
  1 --> 2
  2 --> 3
  3 --> 4
  1 --> 5[failed pre-condition]
  2 --> 5
  3 --> 5
  5 --> 6[else]
Loading

Here's a minimal example with a single precondition. is_admin must be true to delete a user. We're using pseudo-code and simply returning the "delete user" string.

is_admin = true

with true <- is_admin do
  "delete user"
end

The with statement checks is_admin. If true, it returns "delete_user". If any other value, it returns the value of variable is_admin.

flowchart LR
  with --> is_admin --> 3["delete user"]
  is_admin --> 4[is_admin]
Loading

with uses pattern matching to check if the left side of the <- matches the right side. The example above is probably better served using a simple if statement, so let's make it more realistic and store is_admin in a boolean on a user map.

user = %{is_admin: true}

with true <- user do
  "delete user"
end

Because %{is_admin: true} does not match true, the with statement returns %{is_admin: true}. Let's correct that.

user = %{is_admin: true}

with %{is_admin: true} <- user do
  "delete user"
end

Great! That's working. But this is still probably better handled by an if or case statement.

user = %{is_admin: true}

if user.is_admin do
  "delete user"
end

with is ideal for checking a series of preconditions.

Let's change our example to sending an email. To send an email, we need to ensure:

  • The sending user is an admin.
  • The receiving user has an email.
  • The email has a title and a body.

We also need the name of the sender and receiver and their emails.

Before with statements, we might solve this problem using nested case statements. This produces unclear code.

sending_user = %{name: "Batman", email: "[email protected]", is_admin: true}
receiving_user = %{name: "Robin", email: "[email protected]"}
email = %{title: "ROBIN!", body: "WE'RE OUT OF BAT SNACKS!"}

case sending_user do
  %{is_admin: true, name: sender_name, email: sender_email} ->
    case receiving_user do
      %{name: receiver_name, email: receiver_email} ->
        case email do
          %{title: title, body: body} ->
            "from #{sender_name}:#{sender_email} to #{receiver_name}:#{receiver_email} #{title}, #{body}"
        end
    end
end

with replaces the need for nested case statements.

Here's the same code using with. There's still some natural complexity, but with improved the code clarity.

sending_user = %{name: "Batman", email: "[email protected]", is_admin: true}
receiving_user = %{name: "Robin", email: "[email protected]"}
email = %{title: "ROBIN!", body: "WE'RE OUT OF BAT SNACKS!"}

with %{is_admin: true, name: sender_name, email: sender_email} <- sending_user,
     %{name: receiver_name, email: receiver_email} <- receiving_user,
     %{title: title, body: body} <- email do
  "from #{sender_name}:#{sender_email} to #{receiver_name}:#{receiver_email} #{title}, #{body}"
end

Right now, if a value doesn't match the precondition, it returns the value. For example, if the sender is nil, we return nil.

sending_user = nil
receiving_user = %{name: "Robin", email: "[email protected]"}
email = %{title: "ROBIN!", body: "WE'RE OUT OF BAT SNACKS!"}

with %{is_admin: true, name: sender_name, email: sender_email} <- sending_user,
     %{name: receiver_name, email: receiver_email} <- receiving_user,
     %{title: title, body: body} <- email do
  "from #{sender_name}:#{sender_email} to #{receiver_name}:#{receiver_email} #{title}, #{body}"
end

Sometimes we want to return the value. Other times we want to handle the error in an else block.

sending_user = "batman"
receiving_user = %{name: "Robin", email: "[email protected]"}
email = %{title: "ROBIN!", body: "WE'RE OUT OF BAT SNACKS!"}

with %{is_admin: true, name: sender_name, email: sender_email} <- sending_user,
     %{name: receiver_name, email: receiver_email} <- receiving_user,
     %{title: title, body: body} <- email do
  "from #{sender_name}:#{sender_email} to #{receiver_name}:#{receiver_email} #{title}, #{body}"
else
  error -> "Email not sent because #{error} did not match expected format"
end

You can match multiple cases to handle different errors.

sending_user = %{name: "Joker", email: "[email protected]"}
receiving_user = %{name: "Robin", email: "[email protected]"}
email = %{title: "HAHA!", body: "HAHAHAHAHA"}

with %{is_admin: true, name: sender_name, email: sender_email} <- sending_user,
     %{name: receiver_name, email: receiver_email} <- receiving_user,
     %{title: title, body: body} <- email do
  "from #{sender_name}:#{sender_email} to #{receiver_name}:#{receiver_email} #{title}, #{body}"
else
  %{name: "Joker"} -> "Get out of here Joker!"
  error -> "Email not sent because #{error} did not match expected format"
end

with statements can use values from previous conditions in future conditions.

triangle = [3, 3, 3]

with [side1, side2, side3] <- triangle, true <- side1 == side2 && side2 == side3 do
  "all sides are equal!"
end

Your Turn

Use with to check that scores:

  • Has two elements
  • Each element is an integer.
  • Each element is ten or above.

If so, sum the two scores together and return {:ok, score}. If not, return {:error, :invalid}.

Points.tally([10, 20])
{:ok, 30}

Points.tally(10)
{:error, :invalid}

Points.tally([1, 10])
{:error, :invalid}

Points.tally([10, 20, 30])
{:error, :invalid}
Example Solution
defmodule Points do
  def tally(scores) do
    with [score1, score2] <- scores,
        true <- is_integer(score1) and is_integer(score2),
        true <- score1 >= 10 and score2 >= 10 do
      {:ok, score1 + score2}
    else
      _ -> {:error, :invalid}
    end 
  end 
end

Enter your solution below.

defmodule Points do
  def tally(scores) do
  end
end

Commit Your Progress

Run the following in your command line from the beta_curriculum folder to track and save your progress in a Git commit.

$ git add .
$ git commit -m "finish with section"