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

Conversation

angelikatyborska
Copy link
Member

@angelikatyborska angelikatyborska commented Mar 27, 2022

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)

Comment on lines +2 to +4
# 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 :)
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 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.

Copy link
Contributor

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

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 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.

Copy link
Contributor

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!

@angelikatyborska angelikatyborska added the x:size/large Large amount of work label Apr 2, 2022
@angelikatyborska
Copy link
Member Author

@jiegillet The example solution, tests, and instructions are ready for review. Can you take a look?

Copy link
Contributor

@jiegillet jiegillet left a 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.

exercises/concept/dancing-dots/.docs/instructions.md Outdated Show resolved Hide resolved
Comment on lines +2 to +4
# 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 :)
Copy link
Contributor

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)}"}
Copy link
Contributor

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.

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 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"}

Copy link
Contributor

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?

Copy link
Member Author

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)

@github-actions
Copy link
Contributor

github-actions bot commented Apr 8, 2022

Thank you for contributing to exercism/elixir 💜 🎉. This is an automated PR comment 🤖 for the maintainers of this repository that helps with the PR review process. You can safely ignore it and wait for a maintainer to review your changes.

Based on the files changed in this PR, it would be good to pay attention to the following details when reviewing the PR:

  • General steps

    • 🏆 Does this PR need to receive a label with a reputation modifier (x:size/{tiny,small,medium,large,massive})? (A medium reputation amount is awarded by default, see docs)
  • Any exercise changed

    • 👤 Does the author of the PR need to be added as an author or contributor in <exercise>/.meta/config.json (see docs)?
    • 🔬 Do the analyzer and the analyzer comments exist for this exercise? Do they need to be changed?
    • 📜 Does the design file (<exercise>/.meta/design.md) need to be updated to document new implementation decisions?
  • Concept exercise changed

    • 🌲 Do prerequisites and practices in config.json need to be updated?
    • 📖 Does the concept introduction provide all necessary information to solve this exercise?
  • Concept exercise tests changed

    • ⚪️ Are all tests un-skipped?
    • 🔢 Are all tests annotated with @tag task_id?
    • 🐈 Can all tests be understood by reading the test's block only (e.g. no module attributes, no setup functions)?
  • Concept changed

    • 👤 Does the author of the PR need to be added as an author or contributor in <concept>/.meta/config.json (see docs)?
    • 📖 Does the concept introduction provide all necessary information to solve this exercise?

Automated comment created by PR Commenter 🤖.

@angelikatyborska angelikatyborska marked this pull request as ready for review April 8, 2022 13:29
@angelikatyborska
Copy link
Member Author

This is now ready for the final full review.

Copy link
Contributor

@jiegillet jiegillet left a 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.

concepts/behaviours/.meta/config.json Outdated Show resolved Hide resolved
exercises/concept/dancing-dots/.docs/introduction.md Outdated Show resolved Hide resolved
- `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 :)

Comment on lines +2 to +4
# 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 :)
Copy link
Contributor

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
x:size/large Large amount of work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants