-
-
Notifications
You must be signed in to change notification settings - Fork 402
New concept exercise: new-passport
(concept: with
)
#954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
8510c85
structure, design and exemplar
jiegillet 62bfdfb
Tweak exemplar and add tests
jiegillet 92ce649
No require, return retry tuple, naive datetime
jiegillet ce290ed
format
jiegillet cdedf92
move to exemplar
jiegillet fb07c4d
Add with concept
jiegillet 076a954
Fix link, cut introduction
jiegillet 89d039d
Change birthday to Date
jiegillet b902b6e
trailing white space
jiegillet 7cd27c2
Add instructions
jiegillet bedada4
Add hints
jiegillet 3d0efb8
Tweak design.md
jiegillet 3afff39
config.json
jiegillet e6de4d7
trailing whitespace
jiegillet dbd78af
Add prerequisites
jiegillet f57f5a6
Add contributor
jiegillet 154fdf6
prerequisites in design.md
jiegillet df49629
Rework instructions
jiegillet 99bbaa4
Rework hints
jiegillet 96c474e
Rework exemplar
jiegillet 9cd9331
Add prerequisites
jiegillet a1fb53b
trailing whitespace
jiegillet 44c028e
drop match with =
jiegillet f0072c9
Apply suggestions from code review
jiegillet 0baa217
Cut intro
jiegillet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"blurb": "With forms are used to combine chains of matching clauses. If one match fails, the chain exits and returns the non-matched value", | ||
"authors": [ | ||
"jiegillet" | ||
], | ||
"contributors": [ | ||
"angelikatyborska" | ||
] | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# About | ||
|
||
The [special form with][with] provides a way to focus on the "happy path" of a series of potentially failing steps and deal with the failures later. | ||
|
||
Let's say that you want to validate a username with several checks. You might reach out for `case`: | ||
|
||
```elixir | ||
case check_ascii(username) do | ||
{:ok, username} -> | ||
case check_starts_with_letter(username) do | ||
{:ok, username} -> {:ok, username} | ||
{:error, "usernames may only start with a letter"} = err -> err | ||
end | ||
{:error, "usernames may only contain ascii letters"} = err -> err | ||
end | ||
``` | ||
|
||
It might be readable now, but if you add more nested checks, it's going to get messier. In this situation, use `with`: | ||
|
||
|
||
```elixir | ||
with {:ok, username} <- check_ascii(username), | ||
{:ok, username} <- check_starts_with_letter(username), | ||
{:ok, username} <- check_not_a_joke_name(username), | ||
{:ok, username} <- check_if_available(username) do | ||
{:ok, "#{username} is a fine username and you deserve it!"} | ||
end | ||
``` | ||
|
||
At each step, if a clause matches, the chain will continue until the `do` block is executed. If one match fails, the chain stops and the non-matching clause is returned. | ||
|
||
You can use guards in the chain, as well as the `=` operator for regular matches. | ||
|
||
```elixir | ||
with {:ok, id} <- get_id(username), | ||
{:ok, avatar} when is_bitstring(avatar) <- fetch_avatar(id), | ||
avatar_size = bit_size(avatar), | ||
{:ok, image_type} <- check_valid_image_type(avatar) do | ||
{:ok, image_type, avatar_size, avatar} | ||
end | ||
``` | ||
|
||
Finally, you have the option of using an `else` block to catch failed matches and modify the return value. | ||
|
||
```elixir | ||
with {:ok, id} <- get_id(username), | ||
{:ok, avatar} when is_bitstring(avatar) <- fetch_avatar(id), | ||
avatar_size = bit_size(avatar), | ||
{:ok, image_type} <- check_valid_image_type(avatar) do | ||
{:ok, image_type, avatar_size, avatar} | ||
else | ||
:not_found -> | ||
{:error, "invalid username"} | ||
|
||
{:error, "not an image"} -> | ||
{:error, "avatar associated to #{username} is not an image"} | ||
|
||
err -> | ||
err | ||
end | ||
``` | ||
|
||
The `else` is like a `case` statement for any mismatched value of the `with` clause. The first value to match will be used. | ||
|
||
[with]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Introduction | ||
|
||
The [special form with][with] provides a way to focus on the "happy path" of a series of potentially failing steps and deal with the failures later. | ||
|
||
```elixir | ||
with {:ok, id} <- get_id(username), | ||
{:ok, avatar} <- fetch_avatar(id), | ||
{:ok, image_type} <- check_valid_image_type(avatar) do | ||
{:ok, image_type, avatar} | ||
else | ||
:not_found -> | ||
{:error, "invalid username"} | ||
|
||
{:error, "not an image"} -> | ||
{:error, "avatar associated to #{username} is not an image"} | ||
|
||
err -> | ||
err | ||
end | ||
``` | ||
|
||
At each step, if a clause matches, the chain will continue until the `do` block is executed. If one match fails, the chain stops and the non-matching clause is returned. You have the option of using an `else` block to catch failed matches and modify the return value. | ||
|
||
[with]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[ | ||
{ | ||
"url": "https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#with", | ||
"description": "Getting Started guide - With" | ||
}, | ||
{ | ||
"url": "http://learningelixir.joekain.com/learning-elixir-with/", | ||
"description": "Learning Elixir - Learning Elixir's with" | ||
}, | ||
{ | ||
"url": "https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1", | ||
"description": "Kernel.SpecialForms.with/1" | ||
} | ||
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Hints | ||
|
||
## General | ||
|
||
- Read about using `with` in the [official Getting Started guide][getting-started-with]. | ||
- Review the functions available in the [`NaiveDateTime` module][naive-date-time], the [`Date` module][date], and the [`Time` module][time]. | ||
|
||
## 1. Get into the building | ||
|
||
- Match the `:ok` tuple returned by `enter_building/1` in `with` with `<-`. | ||
- In the `do` part of `with`, return an `:ok` tuple with the value you just matched. | ||
- Since you don't need to modify the error, you don't need an `else` block. | ||
|
||
## 2. Go to the information desk and find which counter you should go to | ||
|
||
- Match the `:ok` tuple returned by `find_counter_information/1` in `with` with `<-`. | ||
- Apply the anonymous function your just matched and match the result with `<-`. | ||
- In the `do` part of `with`, return an `:ok` tuple with the counter you obtained. | ||
- Add an `else` block that will expect a `:coffee_break` tuple and return a `:retry` tuple with a `NaiveDateTime`. | ||
- A minute has `60` seconds. | ||
- There is a [built-in function][naive-date-time-add] that adds a given number of seconds to a `NaiveDateTime` struct. | ||
- Other errors should be returned as they are. | ||
|
||
## 3. Go to the counter and get your form stamped | ||
|
||
- Match the `:ok` tuple returned by `stamp_form/3` in `with` with `<-`. | ||
- In the `do` part of `with`, return an `:ok` tuple with the checksum. | ||
|
||
## 4. Receive your new passport | ||
|
||
- In the `do` part of `with`, use `get_new_passport_number/3` and return the result in an `:ok` tuple. | ||
|
||
[with]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1 | ||
[getting-started-with]: https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#with | ||
[naive-date-time]: https://hexdocs.pm/elixir/NaiveDateTime.html | ||
[time]: https://hexdocs.pm/elixir/Time.html | ||
[date]: https://hexdocs.pm/elixir/Date.html | ||
[naive-date-time-add]: https://hexdocs.pm/elixir/NaiveDateTime.html#add/3 | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Instructions | ||
|
||
Your passport is about to expire, so you need to drop by the city office to renew it. You know from previous experience that your city office is not necessarily the easiest to deal with, so you decide to do your best to always "focus on the happy path". | ||
|
||
You print out the form you need to get your new passport, fill it out, jump into your car, drive around the block, park and head to the office. | ||
|
||
All the following tasks will require implementing and extending `get_new_passport/3`. | ||
|
||
## 1. Get into the building | ||
|
||
It turns out that the building is only open in the afternoon, and not at the same time everyday. | ||
|
||
Call the function `enter_building/1` with the current time (given to you as first argument of `get_new_passport/3`). If the building is open, the function will return a tuple with `:ok` and a timestamp that you will need later, otherwise a tuple with `:error` and a message. For now, the happy path can return the `:ok` tuple. | ||
|
||
## 2. Go to the information desk and find which counter you should go to | ||
|
||
The information desk is notorious for taking long coffee breaks. If you are lucky enough to find someone there, they will give you an instruction manual which will explain which counter you need to go to depending on your birth date. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's better 👍 |
||
|
||
Call the function `find_counter_information/1` with the current time. You will get either a tuple with `:ok` and a manual, represented by an anonymous function, or a tuple with `:coffee_break` and more instructions. In your happy path where you receive the manual, apply it to you birthday (second argument of `get_new_passport/3`) and learn which counter to go to. Return an `:ok` tuple with the counter. | ||
|
||
If you get a `:coffee_break` message, return a tuple with `:retry` and a `NaiveDateTime` pointing to 15 minutes after the current time. | ||
|
||
## 3. Go to the counter and get your form stamped | ||
|
||
For some reason, different counters require forms of different colors. Of course, you printed the first one you found on the website, so you focus on your happy path and hope for the best. | ||
|
||
Call the function `stamp_form/3` with the timestamp you received at the entrance, the counter and the form you brought (last argument of `get_new_passport/3`). You will get either a tuple with `:ok` and a checksum that will be used to verify your passport number or a tuple with `:error` and a message. Have your happy path return an `:ok` tuple with the checksum. | ||
|
||
## 4. Receive your new passport | ||
|
||
Finally, you have all the documents you need. | ||
|
||
Call `get_new_passport_number/3` with the timestamp, the counter and the checksum you received earlier. You will receive a string with your final passport number, all that is left to do is to return that string in a tuple with `:ok` and go home. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Introduction | ||
|
||
The [special form with][with] provides a way to focus on the "happy path" of a series of potentially failing steps and deal with the failures later. | ||
|
||
```elixir | ||
with {:ok, id} <- get_id(username), | ||
{:ok, avatar} <- fetch_avatar(id), | ||
{:ok, image_type} <- check_valid_image_type(avatar) do | ||
{:ok, image_type, avatar} | ||
else | ||
:not_found -> | ||
{:error, "invalid username"} | ||
|
||
{:error, "not an image"} -> | ||
{:error, "avatar associated to #{username} is not an image"} | ||
|
||
err -> | ||
err | ||
end | ||
``` | ||
|
||
At each step, if a clause matches, the chain will continue until the `do` block is executed. If one match fails, the chain stops and the non-matching clause is returned. You have the option of using an `else` block to catch failed matches and modify the return value. | ||
|
||
[with]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# The directory Mix will write compiled artifacts to. | ||
/_build/ | ||
|
||
# If you run "mix test --cover", coverage assets end up here. | ||
/cover/ | ||
|
||
# The directory Mix downloads your dependencies sources to. | ||
/deps/ | ||
|
||
# Where third-party dependencies like ExDoc output generated docs. | ||
/doc/ | ||
|
||
# Ignore .fetch files in case you like to edit your project deps locally. | ||
/.fetch | ||
|
||
# If the VM crashes, it generates a dump, let's ignore it too. | ||
erl_crash.dump | ||
|
||
# Also ignore archive artifacts (built via "mix archive.build"). | ||
*.ez | ||
|
||
# Ignore package tarball (built via "mix hex.build"). | ||
match_binary-*.tar | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"blurb": "Learn about `with` to concentrate on the happy path and manage a stressful day of facing bureaucracy.", | ||
jiegillet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"authors": ["jiegillet"], | ||
"contributors": ["angelikatyborska"], | ||
"files": { | ||
"solution": [ | ||
"lib/new_passport.ex" | ||
], | ||
"test": [ | ||
"test/new_passport_test.exs" | ||
], | ||
"exemplar": [ | ||
".meta/exemplar.ex" | ||
] | ||
}, | ||
"language_versions": ">=1.10" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Design | ||
|
||
This exercise should provide a boilerplate with already implemented functions returning `{:ok, any} | {:error, String.t()}` that will be combined in the main function to deal with the multiple possible errors. | ||
|
||
## Learning objectives | ||
|
||
- use `with` syntax | ||
- catch a specific error with `else` | ||
|
||
## Out of scope | ||
|
||
- guard patterns | ||
- realize you don't always need `else` | ||
|
||
## Prerequisites | ||
|
||
- `atoms` | ||
- `cond` | ||
- `tuples` | ||
- `strings` | ||
- `pattern-matching` | ||
- `multiple-clause-functions` | ||
- `guards` | ||
- `errors` | ||
- `dates-and-time` | ||
|
||
## Concepts | ||
|
||
- `with` | ||
|
||
## Analyzer | ||
|
||
- make sure that `with` is used | ||
- make sure that `case` is not used | ||
- make sure that `=` is used within the `with` chain (using `_block_includes`) | ||
- make sure the functions below were not touched |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.