-
-
Notifications
You must be signed in to change notification settings - Fork 400
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
Conversation
# This module is an example of how behaviours can be used in practice. | ||
# You don't need to read it to solve this exercise. | ||
# It's here for the curious :) |
There was a problem hiding this comment.
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 it's a good idea to leave this code here. But one of my biggest problems with understanding behaviours is actually imagining how they can be used once I have some modules that all implement that one behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm with you, it's nice to see how it can be used. render_dots/2
is great because it's very readable, but I think maybe new/2
is a bit too complex... Maybe it could be changed to filter the valid options rather than throwing an error for the sake of simplicity?
(untested idea)
def new(dots, animations_with_opts) do
valid_animations_with_opts =
for {animation_module, opts} <- animations_with_opts,
# using Animation's init/1 callback
{:ok, opts} <- animation_module.init(opts) do
{animation_module, opts}
end
{:ok, %__MODULE__{dots: dots, animations_with_opts: valid_animations_with_opts }}
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, new
was too complex. I think the worst offender there was the reduce_while
call. To make it simpler, I decided to split new
into two functions - new
that just returns a group with no animations, and add_animation
to add a single animation module to the list. That allows getting rid of any Enum
calls.
I didn't want to silently ignore initialization errors in this demo because that would show an anti-pattern IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this new version is great!
@jiegillet The example solution, tests, and instructions are ready for review. Can you take a look? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very very cool! The story is great and there is a lot to learn.
I took my time solving with only the instructions and the Elixir documentation. What a mess!! (the docs, not your instructions) All the ingredients you need for behaviours are scattered everywhere in like 3 or 4 different pages. This really is a needed exercise! I think even a nice blog post about the topic would be great.
For reference, here is my final submission, it's very very similar to yours:
defmodule DancingDots.Animation do
@type dot :: DancingDots.Dot.t()
@type opts :: keyword
@type error :: any
@type frame_number :: pos_integer
@callback init(opts) :: {:ok, opts} | {:error, error}
defmacro __using__(_opts) do
quote do
@behaviour DancingDots.Animation
def init(opts) do
{:ok, opts}
end
defoverridable init: 1
end
end
@callback handle_frame(dot, frame_number, opts) :: dot
end
defmodule DancingDots.Flicker do
use DancingDots.Animation
alias DancingDots.Dot
@impl DancingDots.Animation
def handle_frame(%Dot{opacity: opacity} = dot, frame_number, _opts)
when rem(frame_number, 4) == 0 do
%Dot{dot | opacity: opacity / 2}
end
def handle_frame(%Dot{} = dot, _frame_number, _opts), do: dot
end
defmodule DancingDots.Zoom do
use DancingDots.Animation
alias DancingDots.Dot
@impl DancingDots.Animation
def init(opts) do
velocity = Keyword.get(opts, :velocity)
if is_number(velocity) do
{:ok, opts}
else
{:error, "Expected required option :velocity to be a number, got: #{inspect(velocity)}"}
end
end
@impl DancingDots.Animation
def handle_frame(%Dot{radius: radius} = dot, frame_number, opts) do
n = frame_number - 1
velocity = Keyword.get(opts, :velocity)
%Dot{dot | radius: radius + n * velocity}
end
end
Outside of finding information in the docs, the only place that gave me pause was the error message in DancingDots.Zoom
, I left some comments about that.
# This module is an example of how behaviours can be used in practice. | ||
# You don't need to read it to solve this exercise. | ||
# It's here for the curious :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm with you, it's nice to see how it can be used. render_dots/2
is great because it's very readable, but I think maybe new/2
is a bit too complex... Maybe it could be changed to filter the valid options rather than throwing an error for the sake of simplicity?
(untested idea)
def new(dots, animations_with_opts) do
valid_animations_with_opts =
for {animation_module, opts} <- animations_with_opts,
# using Animation's init/1 callback
{:ok, opts} <- animation_module.init(opts) do
{animation_module, opts}
end
{:ok, %__MODULE__{dots: dots, animations_with_opts: valid_animations_with_opts }}
end
if is_number(velocity) do | ||
{:ok, [velocity: velocity]} | ||
else | ||
{:error, "Expected required option :velocity to be a number, got: #{inspect(velocity)}"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to add one type of errors here: "Missing option :velocity". And some tests for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the suggestion, are you saying there need to be two different errors, one for when velocity was not given, and one for when it's given but not valid? There already is a test for when it's missing:
assert DancingDots.Zoom.init([]) ==
{:error, "Expected required option :velocity to be a number, got: nil"}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was suggesting two different errors, but actually a simple rephrasing might do.
If no :velocity
option is given, the error message says "Expected required option :velocity
to be a number", which feels like you passed something wrong, not nothing at all.
How about:
"The :velocity
option is required, and its value must be a number. Got: nil"
It's a small change, but it feels a bit better maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally, that does sound better. Here: ccfcabe
(#1103)
exercises/concept/dancing-dots/test/dancing_dots/animation_test.exs
Outdated
Show resolved
Hide resolved
Thank you for contributing to Based on the files changed in this PR, it would be good to pay attention to the following details when reviewing the PR:
Automated comment created by PR Commenter 🤖. |
This is now ready for the final full review. |
Co-authored-by: Victor Goff <[email protected]>
Co-authored-by: Victor Goff <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work 🎉
I left some comments, but this is already good enough to merge.
- `structs` | ||
- `ast` | ||
- `enum` | ||
- `import` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are better, yes :)
# This module is an example of how behaviours can be used in practice. | ||
# You don't need to read it to solve this exercise. | ||
# It's here for the curious :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this new version is great!
Co-authored-by: Jie <[email protected]>
Co-authored-by: Jie <[email protected]>
Heavily WIP!I'm opening this PR to signal that I'm working on this.This will be necessary as a prerequisite for the new GenServer exercise (#1076)