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 practice exercise react #820

Merged
merged 13 commits into from
Jul 10, 2021
Merged

New practice exercise react #820

merged 13 commits into from
Jul 10, 2021

Conversation

jiegillet
Copy link
Contributor

This is one a bit harder than the last two. Even combined :)

I would welcome your opinion especially concerning the API, choice of functions, return types and such. For example, I'm sending back the callbacks in a map, but I'm not sure that is idiomatic.

I chose to use cell names as strings instead of atoms, because I know that atoms are not garbage collected and this felt like the kind of system where you would programmatically create new cells, even though it doesn't happen here. But atoms are cleaner, so I'm on the fence.

I left some of the test comments in, because they gave me some insight into the problem, but I can remove them if needed.

Difficulty around 8 or 9 sounds right, but I think I'm mixing the difficulty of designing the tests.

New practice exercise `react`

New practice exercise `react`

New practice exercise `react`

New practice exercise `react`

Change return type
@github-actions
Copy link
Contributor

github-actions bot commented Jul 6, 2021

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?
  • Practice exercise changed

    • 🌲 Do prerequisites, practices, and difficulty in config.json need to be updated?
    • 🧑‍🏫 Are the changes in accordance with the community-wide problem specifiations?
  • Practice exercise tests changed

    • ⚪️ Are all tests except the first one skipped?
    • 📜 Does <exercise>/.meta/tests.toml need updating?

Automated comment created by PR Commenter 🤖.

Copy link
Member

@angelikatyborska angelikatyborska left a comment

Choose a reason for hiding this comment

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

That's a really fascinating exercise 🤔 I'm really curious how people will approach it, and I'm very happy we have more genserver!

config.json Outdated
"recursion"
],
"practices": [
"processes"
Copy link
Member

Choose a reason for hiding this comment

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

I made a note in #559 that this will be a genserver exercise once we have that concept 🙂

defstruct [:name, :inputs, :compute, :value, callbacks: []]

@type t :: %OutputCell{
name: String.t(),
Copy link
Member

@angelikatyborska angelikatyborska Jul 7, 2021

Choose a reason for hiding this comment

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

Do you think cell names should be atoms instead?

Ooops, didn't see the PR description before posting.

Copy link
Member

Choose a reason for hiding this comment

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

I chose to use cell names as strings instead of atoms, because I know that atoms are not garbage collected and this felt like the kind of system where you would programmatically create new cells, even though it doesn't happen here. But atoms are cleaner, so I'm on the fence.

I agree with the concern, let's keep strings. Maybe you could describe this in .meta/design.md? for future maintainer generations :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good suggestion, I will do that. Those notes are only meant for maintainers right?

Copy link
Member

Choose a reason for hiding this comment

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

Yes

test "callbacks should not be called if dependencies change but output value doesn't change" do
# Some incorrect implementations simply mark a compute cell as dirty when a dependency changes,
# then call callbacks on all dirty cells.
# This is incorrect since the specification indicates only to call callbacks on change.
Copy link
Member

Choose a reason for hiding this comment

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

I think those comments are very helpful for understanding the exercise

name: String.t(),
inputs: [String.t()],
compute: (... -> any),
value: any,
Copy link
Member

Choose a reason for hiding this comment

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

If we can allow cells to have any type, should we add a custom test case that tests string cells for example? or boolean cells? (but no error handling of type mismatch)

Problem specifications say:

If your language supports generics, you may consider allowing reactors to act on other types.
Tests for that are not included here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, sure, why not!
Any particular procedure for adding tests?

Copy link
Member

Choose a reason for hiding this comment

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

Since those are our own, we don't need to add anything to the toml file. Maybe mention that we did that in design.md but otherwise nothing special.

end

@doc """
Set the value of an input cell
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this needs a hint about what the function returns?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, the callbacks. Yes, good idea.
Also I have a question about this. It is specified that you can only have specific callback once, so I picked a map as the data structure since the uniqueness of the keys would always enforce that. But that seems like a decision that has to be made by the student. Of course, it's also important for the tests so... I'm not sure.

Copy link
Member

@angelikatyborska angelikatyborska Jul 7, 2021

Choose a reason for hiding this comment

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

After some thought, I find it a bit weird that there are no actual functions involved in the callbacks 🤔.
I'm thinking the API should rather be like this:

React.add_callback(cells, "always_two", fn value -> send(self(), {:callback1, value}))
:ok = React.set_value(cells, "input", 2)
refute_received {:callback, _}, "Expected callback not to be called" # maybe the failure message can be improved

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah nice!
I actually don't have a good intuition of what callbacks are, so this is very helpful. This feels closer to what it should be!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had a go at something that I found sensible, please check :)

Copy link
Member

Choose a reason for hiding this comment

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

Looks much better 👍

@angelikatyborska angelikatyborska requested a review from neenjaw July 7, 2021 07:36
Copy link
Contributor

@neenjaw neenjaw left a comment

Choose a reason for hiding this comment

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

I think this is supposed to be a difficult problem, and I'm okay with it being difficult. I think though, we are perhaps prescribing a very specific approach by defining the structs and using them in the test file.

For me, I'd like to see the interface be a little more agnostic to the solution's data model. A more generic input and allow the student to flex some creativity in their own solution. (To be clear, there is certainly nothing wrong with the approach @jiegillet has taken with their solution to use structs for this, just not sure it should be mandated for every student)

E.g.

# input format
{:input, "input", 1}
# output format
{:output, "output", ["input"], fn a -> a + 1 end}

# Then the interface for the test file becomes:
test "compute cells calculate initial value" do
  {:ok, cells} =
    React.new([
      {:input, "input", 1},
      {:output, "output", ["input"], fn a -> a + 1 end}
    ])

  assert React.get_value(cells, "output") == 2
end

The format could be implemented as a provided type.

Interested in your thoughts

@jiegillet
Copy link
Contributor Author

That makes sense. I use Haskell and Elm a lot, so I'm very type-driven and that's how I structure code, but I should let students make their own design choices. The change you propose is pretty straightforward too, so no problem.

Comment on lines 32 to 33
def add_callback(cells, cell, callback, send) do
end
Copy link
Member

Choose a reason for hiding this comment

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

I think we could improve argument names in the whole stub file E.g. I'm not entirely sure about the name "send". It's specific to message sending, but the callback can do anything. In practice it only makes sense if it does side-effect (because there's nobody that cares about the return value) but the side effects could be not only message sending, but also doing network requests or something 🤷

I would propose:
cell -> cell_name (because this is the string, not the map %{type: :input, value: 1} and definitely not a single instance of the first argument, cells)
callback -> callback_name (because "callback" usually means it's a function)
send -> callback or function or callback_fun?

cells :: pid,
cell :: String.t(),
callback :: String.t(),
send :: (String.t(), any -> :ok)
Copy link
Member

Choose a reason for hiding this comment

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

Does it really matter what the callback returns?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well the callback must send a message, which is of type :ok, otherwise the tests can't be passed.

Copy link
Member

Choose a reason for hiding this comment

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

Must send a message? The callback we use in tests sends a message so that we have an easy way of verifying that it was called, but in general the callback can do anything. It's even in your implementation:

Enum.each(cell.callbacks, fn {name, send} -> send.(name, value) end)

Enum.each -> return value gets discarded, only side effects matter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I get it.

I think what I was doing here is matching the type of the actually callbacks , which are all predefined in the tests as send_callback and are indeed all send, but I get what you are saying, in a real API, you wouldn't restrict the callback type. I will change it :)

end

@doc """
Set the value of an input cell
Copy link
Member

Choose a reason for hiding this comment

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

Looks much better 👍

@doc """
Start a reactive system
"""
@spec new(cells :: [%{atom => any}]) :: {:ok, pid}
Copy link
Member

Choose a reason for hiding this comment

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

The map %{atom => any} will be a very specific map, right? One of:

%{type: :input, name: String.t(), value: any()}
%{type: :output, name: String.t(), inputs: [String.t()], compute: (...) -> any()}

Should we write that in the typespec instead of the generic %{atom => any}?

Copy link
Contributor

@neenjaw neenjaw left a comment

Choose a reason for hiding this comment

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

I think this is a good iteration, but I still think a tuple is preferable to a map/struct for the arguments

Comment on lines 25 to 26
%{type: :input, name: "input", value: 1},
%{type: :output, name: "output", inputs: ["input"], compute: fn a -> a + 1 end}
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand why you are using a map in your data model, to enforce a single entry per key, but I think when sending messages it is much more common to use a tuple for this.

Suggested change
%{type: :input, name: "input", value: 1},
%{type: :output, name: "output", inputs: ["input"], compute: fn a -> a + 1 end}
{:input, "input", 1},
{:output, "output", ["input"], fn a -> a + 1 end}

Copy link
Contributor Author

@jiegillet jiegillet Jul 8, 2021

Choose a reason for hiding this comment

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

Actually, this is not why I don't use a tuple, here I use a map because I want to have named fields, otherwise it's unnecessarily complicated for the student to figure out which value is what... This is just following the data from canonical-data.json.

If there was only two fields, it would be ok, but if I have sometimes 2, sometimes 3, I would always go for clarity.
If you insist I will change to tuples, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, maybe with the @spec specifying which is what it would be OK.

Copy link
Member

Choose a reason for hiding this comment

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

I would rather we keep the maps precisely to have named fields. Tuples will be more confusing IMO, because there will be two different ones of different lengths...

but I think when sending messages

This is not sending messages 🤔

Copy link
Contributor

@neenjaw neenjaw Jul 8, 2021

Choose a reason for hiding this comment

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

I mean "send a message" in a more philospohical sense (archived article since my link is dead (beware, they use the dreaded OOP word, but I think it as a philosophy still stands)). Calling a function is "sending a message". Eg. when invoking the function new I am sending a message to create a system with these constraints.

Copy link
Contributor

@neenjaw neenjaw Jul 8, 2021

Choose a reason for hiding this comment

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

Tuples for this sort of thing is still super common (especially in erlang), and we see tuples of varying lengths for various meanings all the time: {:ok, value1, value2} vs {:error, "message"}

Copy link
Contributor

Choose a reason for hiding this comment

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

But that's why I suggested having a defined type for the tuple so that you can give meaning to the tuple

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about we compromise:

%{type: :input, name: "input", value: 1},
{:output, "output", ["input"], fn a -> a + 1 end}

(🤣 I'm very sorry 😆)

I will reflect on it for a bit. Thank you both for the detailed review.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I see. Ok, I won't insist on maps, if @neenjaw is sure tuples will be better in this context, they will work too if we write a nice spec.

@angelikatyborska
Copy link
Member

I think it's good to merge from my side 🎉 let's see what @neenjaw says

Copy link
Contributor

@neenjaw neenjaw left a comment

Choose a reason for hiding this comment

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

Looks great! 🎉

Comment on lines 2 to 8
@doc """
Start a reactive system
"""
@spec new(cells :: [{:input, String.t(), any} | {:output, String.t(), [String.t()], fun()}]) ::
{:ok, pid}
def new(cells) do
end
Copy link
Contributor

Choose a reason for hiding this comment

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

You could create a type here to break down this complex type

Suggested change
@doc """
Start a reactive system
"""
@spec new(cells :: [{:input, String.t(), any} | {:output, String.t(), [String.t()], fun()}]) ::
{:ok, pid}
def new(cells) do
end
@type input :: {:input, name :: String.t(), value :: any}
@type output ::
{:output, name :: String.t(), dependencies :: list(String.t()), callback :: function()}
@type cell_definition :: input() | output()
@opaque cells :: pid
@doc """
Start a reactive system
"""
@spec new(cell_definitions :: [cell_definition()]) :: {:ok, cells()}
def new(cell_definitions) do
end

@neenjaw neenjaw merged commit a29f3b0 into exercism:main Jul 10, 2021
@jiegillet jiegillet deleted the jie-react branch July 14, 2021 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants