-
-
Notifications
You must be signed in to change notification settings - Fork 402
New concept exercise dancing-dots
(use
and behaviour
)
#1103
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
34 commits
Select commit
Hold shift + click to select a range
43d2f23
Solution first draft
angelikatyborska a1a11e7
Update config
angelikatyborska 98c4faa
Write tests
angelikatyborska ff94fa6
Remove unnecessary code
angelikatyborska c03269d
Write instructions
angelikatyborska a89a48c
Bring back unnecessary demo code
angelikatyborska 7d04120
Integrate demo code into test
angelikatyborska 0ba3a0e
Provide boilerplate solution
angelikatyborska 11dafd0
Rephrase instructions
angelikatyborska c4575bf
Leave notes in intro for later
angelikatyborska 704bb39
Fix module name in mix.exs
angelikatyborska 0f70635
Fix indentation
angelikatyborska 00c6003
Fill out 'out of scope'
angelikatyborska 94c3fd6
Split zoom test into pos and neg velocity
angelikatyborska b15b088
Simplify dot group
angelikatyborska 77d19bd
Bring back comment
angelikatyborska c7122f1
Fill out config
angelikatyborska d5b6d46
Create concept directories
angelikatyborska 2099679
Mention velocity error in instructions
angelikatyborska 6ab5812
Run configlet format
angelikatyborska 3f21f43
Write `use` intro
angelikatyborska faff1bf
Write a behaviours intro
angelikatyborska 1aef4f9
Fill out blurbs
angelikatyborska 0eea950
Write hints
angelikatyborska 5827335
Fix spelling
angelikatyborska 39dee25
Update exercises/concept/dancing-dots/.docs/instructions.md
angelikatyborska def7439
Update exercises/concept/dancing-dots/.docs/instructions.md
angelikatyborska ccfcabe
Improve velocity error message
angelikatyborska eebe603
Update concepts/behaviours/.meta/config.json
angelikatyborska 6f789a2
Update exercises/concept/dancing-dots/.docs/introduction.md
angelikatyborska 5e79019
Add default callback implementation example
angelikatyborska 362ceec
Add analyzer hints
angelikatyborska 57f4ca5
Copy-paste intros and abouts
angelikatyborska 44852a5
Add jie as contributor to concepts
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,9 @@ | ||
{ | ||
"blurb": "Behaviours allow us to define interfaces in a behaviour module that can be later implemented by different callback modules.", | ||
"authors": [ | ||
"angelikatyborska" | ||
], | ||
"contributors": [ | ||
"jiegillet" | ||
] | ||
} |
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,66 @@ | ||
# About | ||
|
||
Behaviours allow us to define interfaces (sets of functions and macros) in a _behaviour module_ that can be later implemented by different _callback modules_. Thanks to the shared interface, those callback modules can be used interchangeably. | ||
|
||
~~~~exercism/note | ||
Note the British spelling of "behaviours". | ||
~~~~ | ||
|
||
## Defining behaviours | ||
|
||
To define a behaviour, we need to create a new module and specify a list of functions that are part of the desired interface. Each function needs to be defined using the `@callback` module attribute. The syntax is identical to a [function typespec][concept-typespecs] (`@spec`). We need to specify a function name, a list of argument types, and all the possible return types. | ||
|
||
```elixir | ||
defmodule Countable do | ||
@callback count(collection :: any) :: pos_integer | ||
end | ||
``` | ||
|
||
## Implementing behaviours | ||
|
||
To add an existing behaviour to our module (create a callback module) we use the `@behaviour` module attribute. Its value should be the name of the behaviour module that we're adding. | ||
|
||
Then, we need to define all the functions (callbacks) that are required by that behaviour module. If we're implementing somebody else's behaviour, like Elixir's built-in `Access` or `GenServer` behaviours, we would find the list of all the behaviour's callbacks in the documentation on [hexdocs.pm][hexdocs]. | ||
|
||
A callback module is not limited to implementing only the functions that are part of its behaviour. It is also possible for a single module to implement multiple behaviours. | ||
|
||
To mark which function comes from which behaviour, we should use the module attribute `@impl` before each function. Its value should be the name of the behaviour module that defines this callback. | ||
|
||
```elixir | ||
defmodule BookCollection do | ||
@behaviour Countable | ||
|
||
defstruct :list, :owner | ||
|
||
@impl Countable | ||
def count(collection) do | ||
Enum.count(collection.list) | ||
end | ||
|
||
def mark_as_read(collection, book) do | ||
# other function unrelated to the Countable behaviour | ||
end | ||
end | ||
``` | ||
|
||
## Default callback implementations | ||
|
||
When defining a behaviour, it is possible to provide a default implementation of a callback. This implementation should be defined in the quoted expression of the `__using__/1` macro. To make it possible for users of the behaviour module to override the default implementation, call the `defoverridable/1` macro after the function implementation. It accepts a keyword list of function names as keys and function arities as values. | ||
|
||
```elixir | ||
defmodule Countable do | ||
@callback count(collection :: any) :: pos_integer | ||
|
||
defmacro __using__(_) do | ||
quote do | ||
@behaviour Countable | ||
def count(collection), do: Enum.count(collection) | ||
defoverridable count: 1 | ||
end | ||
end | ||
end | ||
``` | ||
|
||
Note that defining functions inside of `__using__/1` is discouraged for any other purpose than defining default callback implementations, but you can always define functions in another module and import them in the `__using__/1` macro. | ||
|
||
[concept-typespecs]: https://exercism.org/tracks/elixir/concepts/typespecs |
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,66 @@ | ||
# Introduction | ||
|
||
Behaviours allow us to define interfaces (sets of functions and macros) in a _behaviour module_ that can be later implemented by different _callback modules_. Thanks to the shared interface, those callback modules can be used interchangeably. | ||
|
||
~~~~exercism/note | ||
Note the British spelling of "behaviours". | ||
~~~~ | ||
|
||
## Defining behaviours | ||
|
||
To define a behaviour, we need to create a new module and specify a list of functions that are part of the desired interface. Each function needs to be defined using the `@callback` module attribute. The syntax is identical to a [function typespec][concept-typespecs] (`@spec`). We need to specify a function name, a list of argument types, and all the possible return types. | ||
|
||
```elixir | ||
defmodule Countable do | ||
@callback count(collection :: any) :: pos_integer | ||
end | ||
``` | ||
|
||
## Implementing behaviours | ||
|
||
To add an existing behaviour to our module (create a callback module) we use the `@behaviour` module attribute. Its value should be the name of the behaviour module that we're adding. | ||
|
||
Then, we need to define all the functions (callbacks) that are required by that behaviour module. If we're implementing somebody else's behaviour, like Elixir's built-in `Access` or `GenServer` behaviours, we would find the list of all the behaviour's callbacks in the documentation on [hexdocs.pm][hexdocs]. | ||
|
||
A callback module is not limited to implementing only the functions that are part of its behaviour. It is also possible for a single module to implement multiple behaviours. | ||
|
||
To mark which function comes from which behaviour, we should use the module attribute `@impl` before each function. Its value should be the name of the behaviour module that defines this callback. | ||
|
||
```elixir | ||
defmodule BookCollection do | ||
@behaviour Countable | ||
|
||
defstruct :list, :owner | ||
|
||
@impl Countable | ||
def count(collection) do | ||
Enum.count(collection.list) | ||
end | ||
|
||
def mark_as_read(collection, book) do | ||
# other function unrelated to the Countable behaviour | ||
end | ||
end | ||
``` | ||
|
||
## Default callback implementations | ||
|
||
When defining a behaviour, it is possible to provide a default implementation of a callback. This implementation should be defined in the quoted expression of the `__using__/1` macro. To make it possible for users of the behaviour module to override the default implementation, call the `defoverridable/1` macro after the function implementation. It accepts a keyword list of function names as keys and function arities as values. | ||
|
||
```elixir | ||
defmodule Countable do | ||
@callback count(collection :: any) :: pos_integer | ||
|
||
defmacro __using__(_) do | ||
quote do | ||
@behaviour Countable | ||
def count(collection), do: Enum.count(collection) | ||
defoverridable count: 1 | ||
end | ||
end | ||
end | ||
``` | ||
|
||
Note that defining functions inside of `__using__/1` is discouraged for any other purpose than defining default callback implementations, but you can always define functions in another module and import them in the `__using__/1` macro. | ||
|
||
[concept-typespecs]: https://exercism.org/tracks/elixir/concepts/typespecs |
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/typespecs-and-behaviours.html#behaviours", | ||
"description": "Getting Started - Behaviours" | ||
}, | ||
{ | ||
"url": "https://hexdocs.pm/elixir/typespecs.html#behaviours", | ||
"description": "Documentation - Behaviours" | ||
}, | ||
{ | ||
"url": "https://elixirschool.com/en/lessons/advanced/behaviours", | ||
"description": "Elixir School - Behaviours" | ||
} | ||
] |
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": "The use macro allows us to quickly extend our module with functionally provided by another module.", | ||
"authors": [ | ||
"angelikatyborska" | ||
], | ||
"contributors": [ | ||
"jiegillet" | ||
] | ||
} |
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 @@ | ||
# About | ||
|
||
The `use` macro allows us to quickly extend our module with functionally provided by another module. When we `use` a module, that module can inject code into our module - it can for example define functions, `import` or `alias` other modules, or set module attributes. | ||
|
||
If you ever looked at the test files of some of the Elixir exercises here on Exercism, you most likely noticed that they all start with `use ExUnit.Case`. This single line of code is what makes the macros `test` and `assert` available in the test module. | ||
|
||
```elixir | ||
defmodule LasagnaTest do | ||
use ExUnit.Case | ||
|
||
test "expected minutes in oven" do | ||
assert Lasagna.expected_minutes_in_oven() === 40 | ||
end | ||
end | ||
``` | ||
|
||
## `__using__/1` macro | ||
|
||
What exactly happens when you `use` a module is dictated by that module's `__using__/1` macro. It takes one argument, a keyword list with options, and it returns a [quoted expression][concept-ast]. The code in this quoted expression is inserted into our module when calling `use`. | ||
|
||
```elixir | ||
defmodule ExUnit.Case do | ||
defmacro __using__(opts) do | ||
# some real-life ExUnit code omitted here | ||
quote do | ||
import ExUnit.Assertions | ||
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3] | ||
end | ||
end | ||
end | ||
``` | ||
|
||
The options can be given as a second argument when calling `use`, e.g. `use ExUnit.Case, async: true`. When not given explicitly, they default to an empty list. |
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 @@ | ||
# Introduction | ||
|
||
The `use` macro allows us to quickly extend our module with functionally provided by another module. When we `use` a module, that module can inject code into our module - it can for example define functions, `import` or `alias` other modules, or set module attributes. | ||
|
||
If you ever looked at the test files of some of the Elixir exercises here on Exercism, you most likely noticed that they all start with `use ExUnit.Case`. This single line of code is what makes the macros `test` and `assert` available in the test module. | ||
|
||
```elixir | ||
defmodule LasagnaTest do | ||
use ExUnit.Case | ||
|
||
test "expected minutes in oven" do | ||
assert Lasagna.expected_minutes_in_oven() === 40 | ||
end | ||
end | ||
``` | ||
|
||
## `__using__/1` macro | ||
|
||
What exactly happens when you `use` a module is dictated by that module's `__using__/1` macro. It takes one argument, a keyword list with options, and it returns a [quoted expression][concept-ast]. The code in this quoted expression is inserted into our module when calling `use`. | ||
|
||
```elixir | ||
defmodule ExUnit.Case do | ||
defmacro __using__(opts) do | ||
# some real-life ExUnit code omitted here | ||
quote do | ||
import ExUnit.Assertions | ||
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3] | ||
end | ||
end | ||
end | ||
``` | ||
|
||
The options can be given as a second argument when calling `use`, e.g. `use ExUnit.Case, async: true`. When not given explicitly, they default to an empty list. |
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,10 @@ | ||
[ | ||
{ | ||
"url": "https://elixir-lang.org/getting-started/alias-require-and-import.html#use", | ||
"description": "Getting Started - Use" | ||
}, | ||
{ | ||
"url": "https://hexdocs.pm/elixir/Kernel.html#use/2", | ||
"description": "Documentation - Use" | ||
} | ||
] |
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,52 @@ | ||
# Hints | ||
|
||
## General | ||
|
||
- Read about behaviours in the official [Getting Started guide][getting-started-behaviours]. | ||
- Read about behaviours on [elixirschool.com][elixir-school-behaviours]. | ||
- Read about behaviours in the [documentation][doc-behaviours]. | ||
- Read about `use` in the official [Getting Started guide][getting-started-use]. | ||
- Read about `use` in the [documentation][doc-use]. | ||
|
||
## 1. Define the animation behaviour | ||
|
||
- Use the `@callback` module attribute to define the desired functions. | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- Each callback must specify the function name, list of arguments (their types) and the return value (its type). | ||
- Use the given custom types `dot`, `opts`, `error`, and `frame_number` in the callbacks' definitions. | ||
- Refresh your knowledge of [typespecs][typespec] to help with defining callbacks. | ||
|
||
## 2. Provide a default implementation of the `init/1` callback | ||
|
||
- Define a `__using__/1` macro in the `DacingDots.Animation` module. | ||
- The macros' argument can be ignored. | ||
- The macro must return a [quoted expression][quote]. | ||
- In the quoted expression, use `@behaviour` so that calling `use DacingDots.Animation` sets `DacingDots.Animation` as the using module's behaviour. | ||
- In the quoted expression, implement the `init/1` function. | ||
- The default implementation of the `init/1` function should wrap the given `opts` argument in `:ok` tuple. | ||
- There is [a macro][defoverridable] that can mark a function as overridable. | ||
|
||
## 3. Implement the `Flicker` animation | ||
|
||
- Make use of `DancingDots.Animation` `__using__/1` macro by calling [this one special macro][doc-use] in the `DancingDots.Flicker` module. | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- You do not need to implement the `init/1` function. Its default implementation is enough. | ||
- You need to implement the `handle_frame/3` function. | ||
- To detect "every 4th frame", you can check if the [remainder][rem] when dividing it by 4 is equal to 0. | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## 4. Implement the `Zoom` animation | ||
|
||
- Make use of `DancingDots.Animation` `__using__/1` macro by calling [this one special macro][doc-use] in the `DancingDots.Zoom` module. | ||
- You need to implement both the `init/1` function and the `handle_frame/3` function. | ||
- Use the [`Keyword`][keyword] module to work with the options keyword list. | ||
- There is [a built-in guard][is_number] for checking if a value is a number. | ||
|
||
[getting-started-behaviours]: https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#behaviours | ||
[doc-behaviours]: https://hexdocs.pm/elixir/typespecs.html#behaviours | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[elixir-school-behaviours]: https://elixirschool.com/en/lessons/advanced/behaviours | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[doc-use]: https://hexdocs.pm/elixir/Kernel.html#use/2 | ||
[getting-started-use]: https://elixir-lang.org/getting-started/alias-require-and-import.html#use | ||
[typespec]: https://hexdocs.pm/elixir/typespecs.html | ||
[defoverridable]: https://hexdocs.pm/elixir/Kernel.html#defoverridable/1 | ||
[quote]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2 | ||
[rem]: https://hexdocs.pm/elixir/Kernel.html#rem/2 | ||
[is_number]: https://hexdocs.pm/elixir/Kernel.html#is_number/1 | ||
[keyword]: https://hexdocs.pm/elixir/Keyword.html |
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.