-
-
Notifications
You must be signed in to change notification settings - Fork 402
New concept exercise: take-a-number-deluxe
(genserver
)
#1076
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
40 commits
Select commit
Hold shift + click to select a range
32e05fd
Create boilerplate
angelikatyborska bbb7688
First draft of an example solution
angelikatyborska 759057f
First draft of tests
angelikatyborska 3b957a7
Simplify by removing "currently serving"
angelikatyborska 70cd1bb
Remove unnecessary alias
angelikatyborska 309b342
Add passing user input
angelikatyborska 9a2f021
Fix boilerplate
angelikatyborska 96e04f9
Extend the story
angelikatyborska 5ee9ae4
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska 875e56e
Pass module name to @impl
angelikatyborska 900f38d
Add typespecs to state
angelikatyborska 5e13b0c
Use erlang's :queue
angelikatyborska 12fbbcb
Add jie as contributor
angelikatyborska fc50f25
Merge branch 'main' into add-genserver-exercise
angelikatyborska 0a450e6
Set up configs
angelikatyborska 556827d
Add missing task id
angelikatyborska 03adc6d
Implement auto shutdown with timeouts
angelikatyborska b2d9ffa
Add specs to the boilerplate
angelikatyborska 2171b9a
Write instructions
angelikatyborska 7d3dd34
Write GenServer introduction
angelikatyborska eea8c65
Write hints
angelikatyborska 9c311f4
Write concept blurb
angelikatyborska 121c98a
Add concept to practice exercises
angelikatyborska a44260f
Fix spec mistakes
angelikatyborska 1477627
Build our own queue module
jiegillet dca2d36
Add queue to editor files
jiegillet 0f1d4bc
Fix warning
jiegillet ee0276e
Remove difficulty warning
angelikatyborska c302cde
Add explanation comment to queue
angelikatyborska 740cd0d
Merge branch 'main' into add-genserver-exercise
angelikatyborska 2ec9990
Merge branch 'main' into add-genserver-exercise
angelikatyborska c9f9363
Configlet fmt
angelikatyborska ba73c1f
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska 334ad00
Update exercises/concept/take-a-number-deluxe/.docs/hints.md
angelikatyborska bc89504
Remove erl libs from requirements
angelikatyborska 961700b
Add genserver to circular-buffer
angelikatyborska b6ddbdf
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska 6fc13a8
Fix spec of start_link
angelikatyborska c257071
Use cond in example
angelikatyborska 6274f17
Copy intro to concept intro and about
angelikatyborska 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,6 @@ | ||
{ | ||
"blurb": "GenServer is a behaviour that abstracts common client-server interactions between Elixir processes.", | ||
"authors": [ | ||
"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,141 @@ | ||
# About | ||
|
||
`GenServer` (generic server) is a [behaviour][concept-behaviours] that abstracts common client-server interactions between Elixir processes. | ||
|
||
Remember the receive loop from when we learned about [processes][concept-processes]? The `GenServer` behaviour provides abstractions for implementing such loops, and for exchanging messages with a process that runs such a loop. It makes it easier to keep state and execute asynchronous code. | ||
|
||
Be warned that the name `GenServer` is loaded. It is also used to describe a _module_ that _uses_ the `GenServer` behaviour, as well as a _process_ that was started from a module that _uses_ the `GenServer` behaviour. | ||
|
||
The `GenServer` behaviour defines one required callback, `init/1`, and a few interesting optional callbacks: `handle_call/3`, `handle_cast/2`, and `handle_info/3`. The _clients_ using a `GenServer` aren't supposed to call those callbacks directly. Instead, the `GenServer` module provides functions that clients can use to communicate with a `GenServer` process. | ||
|
||
Often, a single module defines both a _client API_, a set of functions that other parts of your Elixir app can call to communicate with this `GenServer` process, and _server callback implementations_, which contain this `GenServer`'s logic. | ||
|
||
Let's take a look at a simple example of a `GenServer` first, and then learn what each callback means. | ||
|
||
## Examples | ||
|
||
This is an example server that can respond to the repetitive inquisitions of annoying passengers during a long road trip, more exactly the question: "are we there yet?". It keeps track of how many times this question has been asked, returning increasingly more annoyed responses. | ||
|
||
```elixir | ||
defmodule AnnoyingPassengerAutoreponder do | ||
use GenServer | ||
# Client API | ||
|
||
def start_link(init_arg) do | ||
GenServer.start_link(__MODULE__, init_arg) | ||
end | ||
|
||
def are_we_there_yet?(pid) do | ||
GenServer.call(pid, :are_we_there_yet?) | ||
end | ||
|
||
# Server callbacks | ||
|
||
@impl GenServer | ||
def init(_init_arg) do | ||
# the initial count of questions asked is always 0 | ||
state = 0 | ||
{:ok, state} | ||
end | ||
|
||
@impl GenServer | ||
def handle_call(:are_we_there_yet?, _from, state) do | ||
reply = | ||
cond do | ||
state <= 3 -> "No." | ||
state <= 10 -> "I told you #{state} times already. No." | ||
true -> "..." | ||
end | ||
|
||
# increase the count of questions asked | ||
new_state = state + 1 | ||
# reply to the caller | ||
{:reply, reply, new_state} | ||
end | ||
end | ||
``` | ||
|
||
## Callbacks | ||
|
||
### `init/1` | ||
|
||
A server can be started by calling `GenServer.start/3` or `GenServer.start_link/3`. We learned about the difference between those functions in the [links concept][concept-links]. | ||
|
||
Those two functions: | ||
- Accept a module implementing the `GenServer` behavior as the first argument. | ||
- Accept anything as the second argument called `init_arg`. As the name suggest, this argument gets passed to the `init/1` callback. | ||
- Accept an optional third argument with advanced options for running the process that we wont' cover now. | ||
|
||
Starting a server by calling `GenServer.start/3` or `GenServer.start_link/3` will invoke the `init/1` callback in a blocking way. The return value of `init/1` dictates if the server can be started successfully. | ||
|
||
The `init/1` callback usually returns one of those values: | ||
- `{:ok, state}`. The server will start its receive loop using `state` as its initial state. `state` can be of any type. | ||
- `{:stop, reason}`. `reason` can be of any type. The server will not start its receive loop. The process will exit with the given reason. | ||
|
||
There are also more advanced possibilities that we won't cover now. | ||
|
||
If the server's receive loop starts, the functions `GenServer.start/3` and `GenServer.start_link/3` return an `{:ok, pid}` tuple. Otherwise they return `{:error, reason}` | ||
|
||
### `handle_call/3` | ||
|
||
A message that requires a reply can be sent to a server process with `GenServer.call/2`. This function expects the `pid` of a running server process as the first argument, and the message as the second argument. The message can be of any type. | ||
|
||
The `handle_call/3` callback is responsible for handling and responding to synchronous messages. It receives three arguments: | ||
|
||
1. `message` - the value passed as the second argument to `GenServer.call/2`. | ||
2. `from` - the `pid` of the process calling `GenServer.call/2`. Most often this argument can be ignored. | ||
3. `state` - the current state of the server. Remember that its initial value was set in the `init/1` callback. | ||
|
||
The `handle_call/3` usually returns a 3 tuple of `{:reply, reply, state}`. This means that the second element in the tuple, a `reply` that can be of any type, will be sent back to the caller. The third element in the tuple, `state`, is the new state of the server after handling this message. | ||
|
||
There are also more advanced possibilities that we won't cover now. | ||
|
||
~~~~exercism/note | ||
To memorize what this callback does by its name, | ||
think of it as "calling" somebody on the phone. | ||
|
||
If that person is available, you'll receive a reply immediately (synchronously). | ||
~~~~ | ||
|
||
### `handle_cast/2` | ||
|
||
A message that doesn't require a reply can be sent to a server process with `GenServer.cast/2`. Its arguments are identical to those of `GenServer.call/2`. | ||
|
||
The `handle_cast/2` callback is responsible for handling those messages. It receives two arguments, `message` and `state`, which are the same arguments as in the `handle_call/3` callback (except for `from`). | ||
|
||
The `handle_cast/2` usually returns a 2 tuple of `{:noreply, state}`. | ||
|
||
There are also more advanced possibilities that we won't cover now. | ||
|
||
~~~~exercism/note | ||
To memorize what this callback does by its name, | ||
remember that "to cast" also means "to throw". | ||
|
||
If you throw a message in a bottle into the sea, | ||
you don't expect to receive a reply immediately, | ||
or maybe ever. | ||
~~~~ | ||
|
||
### Should I use `call` or `cast`? | ||
|
||
Almost always use `call` even if your client code doesn't need the reply from the server. | ||
|
||
Using `call` waits for the reply, which serves as a backpressure mechanism (to prevent clients from sending too many messages at once). Receiving a reply from the server is also the only way to be sure that the server received and handled the client's message. | ||
|
||
### `handle_info/2` | ||
|
||
Messages can also end up in the server's inbox by means other than calling `GenServer.call/2` or `GenServer.cast/2`, for example calling the plain `send/2` function. | ||
|
||
To handle such messages, use the `handle_info/2` callback. This callback works in exactly the same way as `handle_cast/2`. | ||
|
||
The `GenServer` behaviour provides a catch-all implementation of `handle_info/2` that logs errors about unexpected messages. If you override that default implementation, make sure to always include your own catch-all implementation. If you forget, the server will crash if it receives an unexpected message. | ||
|
||
## Timeouts | ||
|
||
The return value of each of the four callbacks described above can be extended by one more tuple element, a timeout. E.g. instead of returning `{:ok, state}` from `init/1`, return `{:ok, state, timeout}`. | ||
|
||
The timeout can be used to detect a lack of messages in the mailbox for a specific period. If the server returns a timeout from one of its callbacks, and the specified number of milliseconds have elapsed with no message arriving, `handle_info/2` is called with `:timeout` as the first argument. | ||
|
||
[concept-behaviours]: https://exercism.org/tracks/elixir/concepts/behaviours | ||
[concept-processes]: https://exercism.org/tracks/elixir/concepts/processes | ||
[concept-links]: https://exercism.org/tracks/elixir/concepts/links |
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,141 @@ | ||
# Introduction | ||
|
||
`GenServer` (generic server) is a [behaviour][concept-behaviours] that abstracts common client-server interactions between Elixir processes. | ||
|
||
Remember the receive loop from when we learned about [processes][concept-processes]? The `GenServer` behaviour provides abstractions for implementing such loops, and for exchanging messages with a process that runs such a loop. It makes it easier to keep state and execute asynchronous code. | ||
|
||
Be warned that the name `GenServer` is loaded. It is also used to describe a _module_ that _uses_ the `GenServer` behaviour, as well as a _process_ that was started from a module that _uses_ the `GenServer` behaviour. | ||
|
||
The `GenServer` behaviour defines one required callback, `init/1`, and a few interesting optional callbacks: `handle_call/3`, `handle_cast/2`, and `handle_info/3`. The _clients_ using a `GenServer` aren't supposed to call those callbacks directly. Instead, the `GenServer` module provides functions that clients can use to communicate with a `GenServer` process. | ||
|
||
Often, a single module defines both a _client API_, a set of functions that other parts of your Elixir app can call to communicate with this `GenServer` process, and _server callback implementations_, which contain this `GenServer`'s logic. | ||
|
||
Let's take a look at a simple example of a `GenServer` first, and then learn what each callback means. | ||
|
||
## Examples | ||
|
||
This is an example server that can respond to the repetitive inquisitions of annoying passengers during a long road trip, more exactly the question: "are we there yet?". It keeps track of how many times this question has been asked, returning increasingly more annoyed responses. | ||
|
||
```elixir | ||
defmodule AnnoyingPassengerAutoreponder do | ||
use GenServer | ||
# Client API | ||
|
||
def start_link(init_arg) do | ||
GenServer.start_link(__MODULE__, init_arg) | ||
end | ||
|
||
def are_we_there_yet?(pid) do | ||
GenServer.call(pid, :are_we_there_yet?) | ||
end | ||
|
||
# Server callbacks | ||
|
||
@impl GenServer | ||
def init(_init_arg) do | ||
# the initial count of questions asked is always 0 | ||
state = 0 | ||
{:ok, state} | ||
end | ||
|
||
@impl GenServer | ||
def handle_call(:are_we_there_yet?, _from, state) do | ||
reply = | ||
cond do | ||
state <= 3 -> "No." | ||
state <= 10 -> "I told you #{state} times already. No." | ||
true -> "..." | ||
end | ||
|
||
# increase the count of questions asked | ||
new_state = state + 1 | ||
# reply to the caller | ||
{:reply, reply, new_state} | ||
end | ||
end | ||
``` | ||
|
||
## Callbacks | ||
|
||
### `init/1` | ||
|
||
A server can be started by calling `GenServer.start/3` or `GenServer.start_link/3`. We learned about the difference between those functions in the [links concept][concept-links]. | ||
|
||
Those two functions: | ||
- Accept a module implementing the `GenServer` behavior as the first argument. | ||
- Accept anything as the second argument called `init_arg`. As the name suggest, this argument gets passed to the `init/1` callback. | ||
- Accept an optional third argument with advanced options for running the process that we wont' cover now. | ||
|
||
Starting a server by calling `GenServer.start/3` or `GenServer.start_link/3` will invoke the `init/1` callback in a blocking way. The return value of `init/1` dictates if the server can be started successfully. | ||
|
||
The `init/1` callback usually returns one of those values: | ||
- `{:ok, state}`. The server will start its receive loop using `state` as its initial state. `state` can be of any type. | ||
- `{:stop, reason}`. `reason` can be of any type. The server will not start its receive loop. The process will exit with the given reason. | ||
|
||
There are also more advanced possibilities that we won't cover now. | ||
|
||
If the server's receive loop starts, the functions `GenServer.start/3` and `GenServer.start_link/3` return an `{:ok, pid}` tuple. Otherwise they return `{:error, reason}` | ||
|
||
### `handle_call/3` | ||
|
||
A message that requires a reply can be sent to a server process with `GenServer.call/2`. This function expects the `pid` of a running server process as the first argument, and the message as the second argument. The message can be of any type. | ||
|
||
The `handle_call/3` callback is responsible for handling and responding to synchronous messages. It receives three arguments: | ||
|
||
1. `message` - the value passed as the second argument to `GenServer.call/2`. | ||
2. `from` - the `pid` of the process calling `GenServer.call/2`. Most often this argument can be ignored. | ||
3. `state` - the current state of the server. Remember that its initial value was set in the `init/1` callback. | ||
|
||
The `handle_call/3` usually returns a 3 tuple of `{:reply, reply, state}`. This means that the second element in the tuple, a `reply` that can be of any type, will be sent back to the caller. The third element in the tuple, `state`, is the new state of the server after handling this message. | ||
|
||
There are also more advanced possibilities that we won't cover now. | ||
|
||
~~~~exercism/note | ||
To memorize what this callback does by its name, | ||
think of it as "calling" somebody on the phone. | ||
|
||
If that person is available, you'll receive a reply immediately (synchronously). | ||
~~~~ | ||
|
||
### `handle_cast/2` | ||
|
||
A message that doesn't require a reply can be sent to a server process with `GenServer.cast/2`. Its arguments are identical to those of `GenServer.call/2`. | ||
|
||
The `handle_cast/2` callback is responsible for handling those messages. It receives two arguments, `message` and `state`, which are the same arguments as in the `handle_call/3` callback (except for `from`). | ||
|
||
The `handle_cast/2` usually returns a 2 tuple of `{:noreply, state}`. | ||
|
||
There are also more advanced possibilities that we won't cover now. | ||
|
||
~~~~exercism/note | ||
To memorize what this callback does by its name, | ||
remember that "to cast" also means "to throw". | ||
|
||
If you throw a message in a bottle into the sea, | ||
you don't expect to receive a reply immediately, | ||
or maybe ever. | ||
~~~~ | ||
|
||
### Should I use `call` or `cast`? | ||
|
||
Almost always use `call` even if your client code doesn't need the reply from the server. | ||
|
||
Using `call` waits for the reply, which serves as a backpressure mechanism (to prevent clients from sending too many messages at once). Receiving a reply from the server is also the only way to be sure that the server received and handled the client's message. | ||
|
||
### `handle_info/2` | ||
|
||
Messages can also end up in the server's inbox by means other than calling `GenServer.call/2` or `GenServer.cast/2`, for example calling the plain `send/2` function. | ||
|
||
To handle such messages, use the `handle_info/2` callback. This callback works in exactly the same way as `handle_cast/2`. | ||
|
||
The `GenServer` behaviour provides a catch-all implementation of `handle_info/2` that logs errors about unexpected messages. If you override that default implementation, make sure to always include your own catch-all implementation. If you forget, the server will crash if it receives an unexpected message. | ||
|
||
## Timeouts | ||
|
||
The return value of each of the four callbacks described above can be extended by one more tuple element, a timeout. E.g. instead of returning `{:ok, state}` from `init/1`, return `{:ok, state, timeout}`. | ||
|
||
The timeout can be used to detect a lack of messages in the mailbox for a specific period. If the server returns a timeout from one of its callbacks, and the specified number of milliseconds have elapsed with no message arriving, `handle_info/2` is called with `:timeout` as the first argument. | ||
|
||
[concept-behaviours]: https://exercism.org/tracks/elixir/concepts/behaviours | ||
[concept-processes]: https://exercism.org/tracks/elixir/concepts/processes | ||
[concept-links]: https://exercism.org/tracks/elixir/concepts/links |
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/genserver.html", | ||
"description": "Getting Started - GenServer" | ||
}, | ||
{ | ||
"url": "https://hexdocs.pm/elixir/GenServer.html", | ||
"description": "Documentation - Genserver" | ||
}, | ||
{ | ||
"url": "https://en.wikipedia.org/wiki/Actor_model", | ||
"description": "Actor model" | ||
} | ||
] |
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
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.