Skip to content
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

New concept exercise dancing-dots (use and behaviour) #1103

Merged
merged 34 commits into from
Apr 10, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
43d2f23
Solution first draft
angelikatyborska Mar 11, 2022
a1a11e7
Update config
angelikatyborska Mar 27, 2022
98c4faa
Write tests
angelikatyborska Mar 27, 2022
ff94fa6
Remove unnecessary code
angelikatyborska Mar 27, 2022
c03269d
Write instructions
angelikatyborska Apr 2, 2022
a89a48c
Bring back unnecessary demo code
angelikatyborska Apr 2, 2022
7d04120
Integrate demo code into test
angelikatyborska Apr 2, 2022
0ba3a0e
Provide boilerplate solution
angelikatyborska Apr 2, 2022
11dafd0
Rephrase instructions
angelikatyborska Apr 2, 2022
c4575bf
Leave notes in intro for later
angelikatyborska Apr 2, 2022
704bb39
Fix module name in mix.exs
angelikatyborska Apr 2, 2022
0f70635
Fix indentation
angelikatyborska Apr 2, 2022
00c6003
Fill out 'out of scope'
angelikatyborska Apr 8, 2022
94c3fd6
Split zoom test into pos and neg velocity
angelikatyborska Apr 8, 2022
b15b088
Simplify dot group
angelikatyborska Apr 8, 2022
77d19bd
Bring back comment
angelikatyborska Apr 8, 2022
c7122f1
Fill out config
angelikatyborska Apr 8, 2022
d5b6d46
Create concept directories
angelikatyborska Apr 8, 2022
2099679
Mention velocity error in instructions
angelikatyborska Apr 8, 2022
6ab5812
Run configlet format
angelikatyborska Apr 8, 2022
3f21f43
Write `use` intro
angelikatyborska Apr 8, 2022
faff1bf
Write a behaviours intro
angelikatyborska Apr 8, 2022
1aef4f9
Fill out blurbs
angelikatyborska Apr 8, 2022
0eea950
Write hints
angelikatyborska Apr 8, 2022
5827335
Fix spelling
angelikatyborska Apr 9, 2022
39dee25
Update exercises/concept/dancing-dots/.docs/instructions.md
angelikatyborska Apr 9, 2022
def7439
Update exercises/concept/dancing-dots/.docs/instructions.md
angelikatyborska Apr 9, 2022
ccfcabe
Improve velocity error message
angelikatyborska Apr 10, 2022
eebe603
Update concepts/behaviours/.meta/config.json
angelikatyborska Apr 10, 2022
6f789a2
Update exercises/concept/dancing-dots/.docs/introduction.md
angelikatyborska Apr 10, 2022
5e79019
Add default callback implementation example
angelikatyborska Apr 10, 2022
362ceec
Add analyzer hints
angelikatyborska Apr 10, 2022
57f4ca5
Copy-paste intros and abouts
angelikatyborska Apr 10, 2022
44852a5
Add jie as contributor to concepts
angelikatyborska Apr 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions concepts/behaviours/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"blurb": "Behaviours allow us to define interfaces in a behaviour module_that can be later implemented by different callback modules.",
"authors": [
"angelikatyborska"
],
"contributors": [
]
}
3 changes: 3 additions & 0 deletions concepts/behaviours/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# About

TODO: copy from exercise's introduction
3 changes: 3 additions & 0 deletions concepts/behaviours/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Introduction

TODO: copy from exercise's introduction
14 changes: 14 additions & 0 deletions concepts/behaviours/links.json
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"
}
]
8 changes: 8 additions & 0 deletions concepts/use/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"blurb": "The use macro allows us to quickly extend our module with functionally provided by another module.",
"authors": [
"angelikatyborska"
],
"contributors": [
]
}
3 changes: 3 additions & 0 deletions concepts/use/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# About

TODO: copy from exercise's introduction
3 changes: 3 additions & 0 deletions concepts/use/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Introduction

TODO: copy from exercise's introduction
10 changes: 10 additions & 0 deletions concepts/use/links.json
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"
}
]
27 changes: 27 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,23 @@
"enum"
],
"status": "active"
},
{
"slug": "dancing-dots",
"name": "Dancing Dots",
"uuid": "cf9e346b-d809-4c0c-9801-8f59461ece95",
"concepts": [
"behaviours",
"use"
],
"prerequisites": [
"typespecs",
"structs",
"ast",
"enum",
"import"
],
"status": "beta"
}
],
"practice": [
Expand Down Expand Up @@ -2905,6 +2922,11 @@
"slug": "basics",
"name": "Basics"
},
{
"uuid": "8cee26b5-2f55-4b6d-9902-64d10e96a7b6",
"slug": "behaviours",
"name": "Behaviours"
},
{
"uuid": "d291ca4b-7163-43e4-ab02-383904f19c34",
"slug": "binaries",
Expand Down Expand Up @@ -3145,6 +3167,11 @@
"slug": "typespecs",
"name": "Typespecs"
},
{
"uuid": "399e6943-dd79-4de5-a7a6-5df95ee35a85",
"slug": "use",
"name": "Use"
},
{
"uuid": "870a9af1-9354-451c-a0ab-6deada59254a",
"slug": "with",
Expand Down
18 changes: 18 additions & 0 deletions exercises/concept/dancing-dots/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Hints

## General

## 1. Define the animation behaviour

## 2. Provide a default implementation of the `init/1` callback

## 3. Implement the `Flicker` animation

## 4. Implement the `Zoom` animation


[]: https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#behaviours
[]: https://hexdocs.pm/elixir/typespecs.html#behaviours
[]: https://elixirschool.com/en/lessons/advanced/behaviours
[]: https://hexdocs.pm/elixir/Kernel.html#use/2
[]: https://elixir-lang.org/getting-started/alias-require-and-import.html#use
77 changes: 77 additions & 0 deletions exercises/concept/dancing-dots/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Instructions

Your friend, an aspiring artist, reached out to you with a project idea. Let's combine his visual creativity with your technical expertise. It's time to dabble in [generative art][generative-art]!

Constraints help creativity and shorten project deadlines, so you've both agreed to limit your masterpiece to a single shape - the circle. But there's going to be many many circles. And they can move around! You'll call it... dancing dots.

Your friend will definitely want to come up with new elaborate movements for the dots, so you'll start coding by creating an architecture that will allow you to later define new animations easily.

## 1. Define the animation behaviour

Each animation module needs to implement two callbacks: `init/1` and `handle_frame/3`. Define them in the `Animation` module.

Define the `init/1` callback. It should take one argument of type `opts` and return either an `{:ok, opts}` tuple or `{:error, error}` tuple. Implementations of this callback will check if the given options are valid for this particular type of animation.

Define the `handle_frame/3` callbacks. It should take three arguments - the dot, a frame number, and options. It should always return a dot. Implementations of this callback will modify the dot's attributes based on the current frame number and the animation's options.

## 2. Provide a default implementation of the `init/1` callback

The `Animation` behaviour should be easy to incorporate into other modules by calling `use DancingDots.Animation`.

To make that happen, implement the `__using__` macro in the `Animation` module so that it sets the `Animation` module as the other module's behaviour. It should also provide a default implementation of the `init/1` callback. The default implementation of `init/1` should return the given options unchanged.

```elixir
defmodule MyCustomAnimation do
use DancingDots.Animation
end

MyCustomAnimation.init([some_option: true])
# => {:ok, [some_option: true]}
```

## 3. Implement the `Flicker` animation

Use the `Animation` behaviour to implement a flickering animation.

It should use the default `init/1` callback because it doesn't take any options.

Implement the `handle_frame/3` callback. In every 4th frame, it should return the dot with half of its original opacity. In other frames, it should return the dot unchanged.

Frames are counted from `1`. The dot passed to `handle_frame/3` is always the dot in its original state, not in the state from the previous frame.

```elixir
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}

DancingDots.Flicker.handle_frame(dot, 1, [])
# => %DancingDots.Dot{opacity: 1, radius: 24, x: 100, y: 100}

DancingDots.Flicker.handle_frame(dot, 4, [])
# => %DancingDots.Dot{opacity: 0.5, radius: 24, x: 100, y: 100}
```

## 4. Implement the `Zoom` animation

Use the `Animation` behaviour to implement a zooming animation.

This animation takes one option - velocity. Velocity can be any number. If it's negative, the dot gets zoomed out instead of zoomed in.

Implement the `init/1` callback. It should validate that the passed options is a keyword list with a `:velocity` key. The value of velocity must be a number. If it's not a number, return the error `"Expected required option :velocity to be a number, got: #{inspect(velocity)}"`.

Implement the `handle_frame/3` callback. It should return the dot with its radius increased by the `n` times velocity, where `n` is the current frame number minus one.

Frames are counted from `1`. The dot passed to `handle_frame/3` is always the dot in its original state, not in the state from the previous frame.

```elixir
DancingDots.Zoom.init([velocity: nil])
# => {:error, "Expected required option :velocity to be a number, got: nil"}

dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}

DancingDots.Zoom.handle_frame(dot, 1, [velocity: 10])
# => %DancingDots.Dot{radius: 24, opacity: 1, x: 100, y: 100}

DancingDots.Zoom.handle_frame(dot, 2, [velocity: 10])
# => %DancingDots.Dot{radius: 34, opacity: 1, x: 100, y: 100}
```

[generative-art]: https://en.wikipedia.org/wiki/Generative_art
91 changes: 91 additions & 0 deletions exercises/concept/dancing-dots/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Introduction

## `use`

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.

## Behaviours

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 behaviors

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 callbacks. The `__using__/1` macro can be used for this purpose. To make it possible for the user 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.

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-ast]: https://exercism.org/tracks/elixir/concepts/ast
[concept-typespecs]: https://exercism.org/tracks/elixir/concepts/typespecs
[hexdocs]: https://hexdocs.pm
[genserver-callbacks]: https://hexdocs.pm
4 changes: 4 additions & 0 deletions exercises/concept/dancing-dots/.formatter.exs
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}"]
]
24 changes: 24 additions & 0 deletions exercises/concept/dancing-dots/.gitignore
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").
numbers-*.tar

25 changes: 25 additions & 0 deletions exercises/concept/dancing-dots/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"jiegillet"
],
"files": {
"solution": [
"lib/dancing_dots/animation.ex"
],
"test": [
"test/dancing_dots/animation_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
],
"editor": [
"lib/dancing_dots/dot.ex",
"lib/dancing_dots/dot_group.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about behaviours by writing animations for dot-based generative art."
}
26 changes: 26 additions & 0 deletions exercises/concept/dancing-dots/.meta/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Design

## Learning objectives

- Know how to define a behaviour with callbacks
- Know how to provide a default implementation of a callback
- Know how to use a behaviour
- `@impl`

## Out of scope

- `@macrocallback`
- `@optional_callbacks`

## Concepts

- `behaviours`
- `use`

## Prerequisites

- `typespecs`
- `structs`
- `ast`
- `enum`
- `import`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need for an analyzer as well?

The only thing I can thing of is that DancingDots.Zoom.init/1 uses Keyword and is_number but it's very minor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if those two things even matter, using the access behaviour (opts[:velocity]) is also fine. I guess the only "bad idea" that technically works would be pattern matching on the keyword list. As for is_number, we could add more tests that would assert that it also allows floats or something, but I don't think that's really necessary.

I could think of two things, relevant to the concept:

362ceec (#1103)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are better, yes :)

Loading