Skip to content

Commit d4de16d

Browse files
angelikatyborskakotpjiegillet
authored
New concept exercise dancing-dots (use and behaviour) (#1103)
Co-authored-by: Victor Goff <[email protected]> Co-authored-by: Jie <[email protected]>
1 parent 5f741e8 commit d4de16d

File tree

23 files changed

+951
-0
lines changed

23 files changed

+951
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"blurb": "Behaviours allow us to define interfaces in a behaviour module that can be later implemented by different callback modules.",
3+
"authors": [
4+
"angelikatyborska"
5+
],
6+
"contributors": [
7+
"jiegillet"
8+
]
9+
}

concepts/behaviours/about.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# About
2+
3+
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.
4+
5+
~~~~exercism/note
6+
Note the British spelling of "behaviours".
7+
~~~~
8+
9+
## Defining behaviours
10+
11+
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.
12+
13+
```elixir
14+
defmodule Countable do
15+
@callback count(collection :: any) :: pos_integer
16+
end
17+
```
18+
19+
## Implementing behaviours
20+
21+
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.
22+
23+
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].
24+
25+
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.
26+
27+
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.
28+
29+
```elixir
30+
defmodule BookCollection do
31+
@behaviour Countable
32+
33+
defstruct :list, :owner
34+
35+
@impl Countable
36+
def count(collection) do
37+
Enum.count(collection.list)
38+
end
39+
40+
def mark_as_read(collection, book) do
41+
# other function unrelated to the Countable behaviour
42+
end
43+
end
44+
```
45+
46+
## Default callback implementations
47+
48+
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.
49+
50+
```elixir
51+
defmodule Countable do
52+
@callback count(collection :: any) :: pos_integer
53+
54+
defmacro __using__(_) do
55+
quote do
56+
@behaviour Countable
57+
def count(collection), do: Enum.count(collection)
58+
defoverridable count: 1
59+
end
60+
end
61+
end
62+
```
63+
64+
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.
65+
66+
[concept-typespecs]: https://exercism.org/tracks/elixir/concepts/typespecs
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Introduction
2+
3+
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.
4+
5+
~~~~exercism/note
6+
Note the British spelling of "behaviours".
7+
~~~~
8+
9+
## Defining behaviours
10+
11+
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.
12+
13+
```elixir
14+
defmodule Countable do
15+
@callback count(collection :: any) :: pos_integer
16+
end
17+
```
18+
19+
## Implementing behaviours
20+
21+
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.
22+
23+
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].
24+
25+
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.
26+
27+
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.
28+
29+
```elixir
30+
defmodule BookCollection do
31+
@behaviour Countable
32+
33+
defstruct :list, :owner
34+
35+
@impl Countable
36+
def count(collection) do
37+
Enum.count(collection.list)
38+
end
39+
40+
def mark_as_read(collection, book) do
41+
# other function unrelated to the Countable behaviour
42+
end
43+
end
44+
```
45+
46+
## Default callback implementations
47+
48+
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.
49+
50+
```elixir
51+
defmodule Countable do
52+
@callback count(collection :: any) :: pos_integer
53+
54+
defmacro __using__(_) do
55+
quote do
56+
@behaviour Countable
57+
def count(collection), do: Enum.count(collection)
58+
defoverridable count: 1
59+
end
60+
end
61+
end
62+
```
63+
64+
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.
65+
66+
[concept-typespecs]: https://exercism.org/tracks/elixir/concepts/typespecs

concepts/behaviours/links.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"url": "https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#behaviours",
4+
"description": "Getting Started - Behaviours"
5+
},
6+
{
7+
"url": "https://hexdocs.pm/elixir/typespecs.html#behaviours",
8+
"description": "Documentation - Behaviours"
9+
},
10+
{
11+
"url": "https://elixirschool.com/en/lessons/advanced/behaviours",
12+
"description": "Elixir School - Behaviours"
13+
}
14+
]

concepts/use/.meta/config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"blurb": "The use macro allows us to quickly extend our module with functionally provided by another module.",
3+
"authors": [
4+
"angelikatyborska"
5+
],
6+
"contributors": [
7+
"jiegillet"
8+
]
9+
}

concepts/use/about.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# About
2+
3+
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.
4+
5+
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.
6+
7+
```elixir
8+
defmodule LasagnaTest do
9+
use ExUnit.Case
10+
11+
test "expected minutes in oven" do
12+
assert Lasagna.expected_minutes_in_oven() === 40
13+
end
14+
end
15+
```
16+
17+
## `__using__/1` macro
18+
19+
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`.
20+
21+
```elixir
22+
defmodule ExUnit.Case do
23+
defmacro __using__(opts) do
24+
# some real-life ExUnit code omitted here
25+
quote do
26+
import ExUnit.Assertions
27+
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
28+
end
29+
end
30+
end
31+
```
32+
33+
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.

concepts/use/introduction.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Introduction
2+
3+
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.
4+
5+
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.
6+
7+
```elixir
8+
defmodule LasagnaTest do
9+
use ExUnit.Case
10+
11+
test "expected minutes in oven" do
12+
assert Lasagna.expected_minutes_in_oven() === 40
13+
end
14+
end
15+
```
16+
17+
## `__using__/1` macro
18+
19+
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`.
20+
21+
```elixir
22+
defmodule ExUnit.Case do
23+
defmacro __using__(opts) do
24+
# some real-life ExUnit code omitted here
25+
quote do
26+
import ExUnit.Assertions
27+
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
28+
end
29+
end
30+
end
31+
```
32+
33+
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.

concepts/use/links.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"url": "https://elixir-lang.org/getting-started/alias-require-and-import.html#use",
4+
"description": "Getting Started - Use"
5+
},
6+
{
7+
"url": "https://hexdocs.pm/elixir/Kernel.html#use/2",
8+
"description": "Documentation - Use"
9+
}
10+
]

config.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,23 @@
614614
"enum"
615615
],
616616
"status": "active"
617+
},
618+
{
619+
"slug": "dancing-dots",
620+
"name": "Dancing Dots",
621+
"uuid": "cf9e346b-d809-4c0c-9801-8f59461ece95",
622+
"concepts": [
623+
"behaviours",
624+
"use"
625+
],
626+
"prerequisites": [
627+
"typespecs",
628+
"structs",
629+
"ast",
630+
"enum",
631+
"import"
632+
],
633+
"status": "beta"
617634
}
618635
],
619636
"practice": [
@@ -2905,6 +2922,11 @@
29052922
"slug": "basics",
29062923
"name": "Basics"
29072924
},
2925+
{
2926+
"uuid": "8cee26b5-2f55-4b6d-9902-64d10e96a7b6",
2927+
"slug": "behaviours",
2928+
"name": "Behaviours"
2929+
},
29082930
{
29092931
"uuid": "d291ca4b-7163-43e4-ab02-383904f19c34",
29102932
"slug": "binaries",
@@ -3145,6 +3167,11 @@
31453167
"slug": "typespecs",
31463168
"name": "Typespecs"
31473169
},
3170+
{
3171+
"uuid": "399e6943-dd79-4de5-a7a6-5df95ee35a85",
3172+
"slug": "use",
3173+
"name": "Use"
3174+
},
31483175
{
31493176
"uuid": "870a9af1-9354-451c-a0ab-6deada59254a",
31503177
"slug": "with",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Hints
2+
3+
## General
4+
5+
- Read about behaviours in the official [Getting Started guide][getting-started-behaviours].
6+
- Read about behaviours on [elixirschool.com][elixir-school-behaviours].
7+
- Read about behaviours in the [documentation][doc-behaviours].
8+
- Read about `use` in the official [Getting Started guide][getting-started-use].
9+
- Read about `use` in the [documentation][doc-use].
10+
11+
## 1. Define the animation behaviour
12+
13+
- Use the `@callback` module attribute to define the desired functions.
14+
- Each callback must specify the function name, list of arguments (their types) and the return value (its type).
15+
- Use the given custom types `dot`, `opts`, `error`, and `frame_number` in the callbacks' definitions.
16+
- Refresh your knowledge of [typespecs][typespec] to help with defining callbacks.
17+
18+
## 2. Provide a default implementation of the `init/1` callback
19+
20+
- Define a `__using__/1` macro in the `DacingDots.Animation` module.
21+
- The macros' argument can be ignored.
22+
- The macro must return a [quoted expression][quote].
23+
- In the quoted expression, use `@behaviour` so that calling `use DacingDots.Animation` sets `DacingDots.Animation` as the using module's behaviour.
24+
- In the quoted expression, implement the `init/1` function.
25+
- The default implementation of the `init/1` function should wrap the given `opts` argument in `:ok` tuple.
26+
- There is [a macro][defoverridable] that can mark a function as overridable.
27+
28+
## 3. Implement the `Flicker` animation
29+
30+
- Make use of `DancingDots.Animation` `__using__/1` macro by calling [this one special macro][doc-use] in the `DancingDots.Flicker` module.
31+
- You do not need to implement the `init/1` function. Its default implementation is enough.
32+
- You need to implement the `handle_frame/3` function.
33+
- To detect "every 4th frame", you can check if the [remainder][rem] when dividing it by 4 is equal to 0.
34+
35+
## 4. Implement the `Zoom` animation
36+
37+
- Make use of `DancingDots.Animation` `__using__/1` macro by calling [this one special macro][doc-use] in the `DancingDots.Zoom` module.
38+
- You need to implement both the `init/1` function and the `handle_frame/3` function.
39+
- Use the [`Keyword`][keyword] module to work with the options keyword list.
40+
- There is [a built-in guard][is_number] for checking if a value is a number.
41+
42+
[getting-started-behaviours]: https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#behaviours
43+
[doc-behaviours]: https://hexdocs.pm/elixir/typespecs.html#behaviours
44+
[elixir-school-behaviours]: https://elixirschool.com/en/lessons/advanced/behaviours
45+
[doc-use]: https://hexdocs.pm/elixir/Kernel.html#use/2
46+
[getting-started-use]: https://elixir-lang.org/getting-started/alias-require-and-import.html#use
47+
[typespec]: https://hexdocs.pm/elixir/typespecs.html
48+
[defoverridable]: https://hexdocs.pm/elixir/Kernel.html#defoverridable/1
49+
[quote]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2
50+
[rem]: https://hexdocs.pm/elixir/Kernel.html#rem/2
51+
[is_number]: https://hexdocs.pm/elixir/Kernel.html#is_number/1
52+
[keyword]: https://hexdocs.pm/elixir/Keyword.html

0 commit comments

Comments
 (0)