With
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.8.0", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Navigation
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]
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
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]
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]
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: "notbrucewayne@bat.net", is_admin: true}
receiving_user = %{name: "Robin", email: "boywonder@bat.net"}
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: "notbrucewayne@bat.net", is_admin: true}
receiving_user = %{name: "Robin", email: "boywonder@bat.net"}
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: "boywonder@bat.net"}
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: "boywonder@bat.net"}
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: "joker@jokesonyou.haha"}
receiving_user = %{name: "Robin", email: "boywonder@bat.net"}
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 sum the numbers in two deeply nested maps. Return {:error, :invalid} if either input is invalid.
Sum.sum_maps(%{value: 10}, %{value: 20})
{:ok, 20}
Example Solution
defmodule Sum do
def sum_maps(map1, map2) do
with %{value: value1} <- map1, %{value: value2} <- map2 do
{:ok, value1 + value2}
else
_ -> {:error, :invalid}
end
end
end
defmodule Sum do
@doc """
Sum two maps. Return {:ok, value} if valid, and {:error, :invalid} if either input is invalid.
## Examples
iex> Sum.sum_maps(%{value: 10}, %{value: 10})
{:ok, 20}
iex> Sum.sum_maps(%{value: 20}, 10)
{:error, :invalid}
"""
def sum_maps(map1, map2) do
end
end
Mark As Completed
file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")
save_name =
case Path.basename(__DIR__) do
"reading" -> "with_reading"
"exercises" -> "with_exercise"
end
progress_path = __DIR__ <> "/../progress.json"
existing_progress = File.read!(progress_path) |> Jason.decode!()
default = Map.get(existing_progress, save_name, false)
form =
Kino.Control.form(
[
completed: input = Kino.Input.checkbox("Mark As Completed", default: default)
],
report_changes: true
)
Task.async(fn ->
for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
File.write!(
progress_path,
Jason.encode!(Map.put(existing_progress, save_name, completed), pretty: true)
)
end
end)
form
Commit Your Progress
Run the following in your command line from the curriculum folder to track and save your progress in a Git commit.
Ensure that you do not already have undesired or unrelated changes by running git status or by checking the source control tab in Visual Studio Code.
$ git checkout -b with-reading
$ git add .
$ git commit -m "finish with reading"
$ git push origin with-reading
Create a pull request from your with-reading branch to your solutions branch.
Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker.
DockYard Academy Students Only:
Notify your teacher by including @BrooklinJazz in your PR description to get feedback.
You (or your teacher) may merge your PR into your solutions branch after review.
If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.
Up Next
| Previous | Next |
|---|---|
| Guards | Message Validation |