diff --git a/config.json b/config.json index 60d37cb..c17d22f 100644 --- a/config.json +++ b/config.json @@ -510,6 +510,14 @@ "practices": [], "prerequisites": [], "difficulty": 8 + }, + { + "slug": "react", + "name": "React", + "uuid": "ea4b0ade-0c71-44b2-99ec-9321fffe19ff", + "practices": [], + "prerequisites": [], + "difficulty": 8 } ] }, diff --git a/exercises/practice/react/.busted b/exercises/practice/react/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/react/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/react/.docs/instructions.md b/exercises/practice/react/.docs/instructions.md new file mode 100644 index 0000000..1b9a175 --- /dev/null +++ b/exercises/practice/react/.docs/instructions.md @@ -0,0 +1,11 @@ +# Instructions + +Implement a basic reactive system. + +Reactive programming is a programming paradigm that focuses on how values are computed in terms of each other to allow a change to one value to automatically propagate to other values, like in a spreadsheet. + +Implement a basic reactive system with cells with settable values ("input" cells) and cells with values computed in terms of other cells ("compute" cells). +Implement updates so that when an input value is changed, values propagate to reach a new stable system state. + +In addition, compute cells should allow for registering change notification callbacks. +Call a cell’s callbacks when the cell’s value in a new stable state has changed from the previous stable state. diff --git a/exercises/practice/react/.meta/config.json b/exercises/practice/react/.meta/config.json new file mode 100644 index 0000000..ce5ca82 --- /dev/null +++ b/exercises/practice/react/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "react.moon" + ], + "test": [ + "react_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "blurb": "Implement a basic reactive system." +} diff --git a/exercises/practice/react/.meta/example.moon b/exercises/practice/react/.meta/example.moon new file mode 100644 index 0000000..7f7ee90 --- /dev/null +++ b/exercises/practice/react/.meta/example.moon @@ -0,0 +1,69 @@ +class Cell + new: => + @val = nil + @listeners = {} + + get_value: => @val + + store_value: (value) => @val = value + + add_listener: (listener) => + table.insert @listeners, listener + + recompute_listeners: => + listener\recompute! for listener in *@listeners + + fire_listener_callbacks: => + listener\fire_callbacks! for listener in *@listeners + + +class InputCell extends Cell + new: (value) => + super! + @val = value + + set_value: (value) => + @store_value value + @recompute_listeners! + @fire_listener_callbacks! + + +class ComputeCell extends Cell + new: (...) => + super! + @inputs = {...} + @formula = table.remove @inputs + @callbacks = {} + + input\add_listener self for input in *@inputs + @compute! + @previous = @get_value! + + compute: => + values = [input\get_value! for input in *@inputs] + -- note the odd parentheses: have to extract the formula from the instance *first* + @store_value (@formula) table.unpack values + + recompute: => + @compute! + @recompute_listeners! + + fire_callbacks: => + val = @get_value! + return if val == @previous + + @previous = val + cb val for cb in *@callbacks + @fire_listener_callbacks! + + watch: (callback) => + table.insert @callbacks, callback + + unwatch: (callback) => + for i, cb in ipairs @callbacks + if cb == callback + table.remove @callbacks, i + break + + +{ :InputCell, :ComputeCell } diff --git a/exercises/practice/react/.meta/tests.toml b/exercises/practice/react/.meta/tests.toml new file mode 100644 index 0000000..9b29a0a --- /dev/null +++ b/exercises/practice/react/.meta/tests.toml @@ -0,0 +1,52 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[c51ee736-d001-4f30-88d1-0c8e8b43cd07] +description = "input cells have a value" + +[dedf0fe0-da0c-4d5d-a582-ffaf5f4d0851] +description = "an input cell's value can be set" + +[5854b975-f545-4f93-8968-cc324cde746e] +description = "compute cells calculate initial value" + +[25795a3d-b86c-4e91-abe7-1c340e71560c] +description = "compute cells take inputs in the right order" + +[c62689bf-7be5-41bb-b9f8-65178ef3e8ba] +description = "compute cells update value when dependencies are changed" + +[5ff36b09-0a88-48d4-b7f8-69dcf3feea40] +description = "compute cells can depend on other compute cells" + +[abe33eaf-68ad-42a5-b728-05519ca88d2d] +description = "compute cells fire callbacks" + +[9e5cb3a4-78e5-4290-80f8-a78612c52db2] +description = "callback cells only fire on change" + +[ada17cb6-7332-448a-b934-e3d7495c13d3] +description = "callbacks do not report already reported values" + +[ac271900-ea5c-461c-9add-eeebcb8c03e5] +description = "callbacks can fire from multiple cells" + +[95a82dcc-8280-4de3-a4cd-4f19a84e3d6f] +description = "callbacks can be added and removed" + +[f2a7b445-f783-4e0e-8393-469ab4915f2a] +description = "removing a callback multiple times doesn't interfere with other callbacks" + +[daf6feca-09e0-4ce5-801d-770ddfe1c268] +description = "callbacks should only be called once even if multiple dependencies change" + +[9a5b159f-b7aa-4729-807e-f1c38a46d377] +description = "callbacks should not be called if dependencies change but output value doesn't change" diff --git a/exercises/practice/react/react.moon b/exercises/practice/react/react.moon new file mode 100644 index 0000000..ccbddb6 --- /dev/null +++ b/exercises/practice/react/react.moon @@ -0,0 +1,2 @@ +-- This exercise is rated "difficult". +-- You are expected to code what is needed by the tests. diff --git a/exercises/practice/react/react_spec.moon b/exercises/practice/react/react_spec.moon new file mode 100644 index 0000000..68ac9a4 --- /dev/null +++ b/exercises/practice/react/react_spec.moon @@ -0,0 +1,128 @@ +import InputCell, ComputeCell from require 'react' + +describe 'react', -> + it 'input cells have a value', -> + input = InputCell 2 + assert.are.equal 2, input\get_value! + + pending "an input cell's value can be set", -> + input = InputCell 4 + input\set_value 20 + assert.are.equal 20, input\get_value! + + pending 'compute cells calculate initial value', -> + input = InputCell 1 + output = ComputeCell input, (x) -> x + 1 + assert.are.equal 2, output\get_value! + + pending 'compute cells take inputs in the right order', -> + one = InputCell 1 + two = InputCell 2 + output = ComputeCell one, two, (x, y) -> x + y * 10 + assert.are.equal 21, output\get_value! + + pending 'compute cells update value when dependencies are changed', -> + input = InputCell 1 + output = ComputeCell input, (x) -> x + 1 + + input\set_value 3 + assert.are.equal 4, output\get_value! + + pending 'compute cells can depend on other compute cells', -> + input = InputCell 1 + times_two = ComputeCell input, (x) -> x * 2 + times_thirty = ComputeCell input, (x) -> x * 30 + + output = ComputeCell times_two, times_thirty, (x, y) -> x + y + assert.are.equal 32, output\get_value! + + input\set_value 3 + assert.are.equal 96, output\get_value! + + pending 'compute cells fire callbacks', -> + input = InputCell 1 + output = ComputeCell input, (x) -> x + 1 + + -- "Spies" are documented here: https://lunarmodules.github.io/busted/#spies-mocks-stubs + callback = spy.new(->) -- the argument is an empty function + + output\watch callback + input\set_value 3 + assert.spy(callback).was_called 1 + assert.spy(callback).was_called_with 4 + + pending 'callbacks only fire on change', -> + input = InputCell 1 + output = ComputeCell input, (x) -> if x < 3 then 111 else 222 + callback = spy.new(->) + + output\watch callback + + input\set_value 2 + assert.spy(callback).was_called 0 + + input\set_value 4 + assert.spy(callback).was_called 1 + assert.spy(callback).was_called_with 222 + + pending 'callbacks can be added and removed', -> + input = InputCell 11 + output = ComputeCell input, (x) -> x + 1 + callback1 = spy.new(->) + callback2 = spy.new(->) + callback3 = spy.new(->) + + output\watch callback1 + output\watch callback2 + input\set_value 31 + + output\unwatch callback1 + output\watch callback3 + input\set_value 41 + + assert.spy(callback1).was_called 1 + assert.spy(callback1).was_called_with 32 + assert.spy(callback2).was_called 2 + assert.spy(callback2).was_called_with 42 + assert.spy(callback3).was_called 1 + assert.spy(callback3).was_called_with 42 + + pending "removing a callback multiple times doesn't interfere with other callbacks", -> + input = InputCell 1 + output = ComputeCell input, (x) -> x + 1 + callback1 = spy.new(->) + callback2 = spy.new(->) + + output\watch callback1 + output\watch callback2 + for i = 1, 10 + output\unwatch callback1 + input\set_value 2 + + assert.spy(callback1).was_called 0 + assert.spy(callback2).was_called 1 + assert.spy(callback2).was_called_with 3 + + pending 'callbacks only called once even if multiple inputs change', -> + input = InputCell 1 + plus_one = ComputeCell input, (x) -> x + 1 + minus_one1 = ComputeCell input, (x) -> x - 1 + minus_one2 = ComputeCell minus_one1, (x) -> x - 1 + output = ComputeCell plus_one, minus_one2, (x, y) -> x * y + callback = spy.new(->) + + output\watch callback + input\set_value 4 + assert.spy(callback).was_called 1 + assert.spy(callback).was_called_with 10 + + pending "callbacks not called if inputs change but output doesn't", -> + input = InputCell 1 + plus_one = ComputeCell input, (x) -> x + 1 + minus_one = ComputeCell input, (x) -> x - 1 + always_two = ComputeCell plus_one, minus_one, (x, y) -> x - y + callback = spy.new(->) + + always_two\watch callback + input\set_value i for i = 1, 10 + assert.spy(callback).was_called 0