diff --git a/config.json b/config.json index cb3e874bf3..00f691db7a 100644 --- a/config.json +++ b/config.json @@ -864,6 +864,14 @@ "prerequisites": [], "difficulty": 6 }, + { + "slug": "react", + "name": "React", + "uuid": "ce90bd42-f7b9-4f64-9096-d6e3856d8fa8", + "practices": [], + "prerequisites": [], + "difficulty": 6 + }, { "slug": "say", "name": "Say", diff --git a/exercises/practice/react/.docs/instructions.md b/exercises/practice/react/.docs/instructions.md new file mode 100644 index 0000000000..1b9a175d0b --- /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 0000000000..d4816d06ac --- /dev/null +++ b/exercises/practice/react/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "metif12" + ], + "files": { + "solution": [ + "react.v" + ], + "test": [ + "run_test.v" + ], + "example": [ + ".meta/example.v" + ] + }, + "blurb": "Implement a basic reactive system." +} diff --git a/exercises/practice/react/.meta/example.v b/exercises/practice/react/.meta/example.v new file mode 100644 index 0000000000..116aeb7a03 --- /dev/null +++ b/exercises/practice/react/.meta/example.v @@ -0,0 +1,209 @@ +module main + +// A Callback is called with the new value of a cell whenever the value of that +// cell in a new stable state differs from the value it had in the previous one. +type Callback = fn (int) + +// A Compute derives the value of a compute cell from the values of the cells it +// depends on, given in the order those cells were registered. +type Compute = fn ([]int) int + +// One cell of a reaction. An input cell only holds a value, a compute cell also +// holds the formula that derives that value from other cells. +struct Cell { +mut: + name string + is_input bool + inputs []string + compute ?Compute + value int + callbacks map[string]Callback +} + +// A Reaction is a set of named cells. Giving an input cell a new value makes +// every cell that depends on it follow along, so the reaction always ends up in +// a new stable state before a change is reported. +struct Reaction { +mut: + cells map[string]Cell +} + +// new_reaction returns a reaction that has no cells yet. +fn new_reaction() &Reaction { + return &Reaction{ + cells: map[string]Cell{} + } +} + +// create_input adds an input cell called `name` that holds `initial_value`. +// +// > returns an error when a cell called `name` is there already +fn (mut r Reaction) create_input(name string, initial_value int) ! { + if name in r.cells { + return error('a cell called ${name} is there already') + } + r.cells[name] = Cell{ + name: name + is_input: true + value: initial_value + callbacks: map[string]Callback{} + } +} + +// create_compute adds a compute cell called `name`. Its value is `compute` +// applied to the values of `inputs`, in the order they are given, and it is +// calculated as soon as the cell is added so it holds a value right away. +// +// > returns an error when a cell called `name` is there already, or when one of +// > `inputs` is not a cell of this reaction +fn (mut r Reaction) create_compute(name string, inputs []string, compute Compute) ! { + if name in r.cells { + return error('a cell called ${name} is there already') + } + for input in inputs { + if input !in r.cells { + return error('there is no cell called ${input} to compute ${name} from') + } + } + r.cells[name] = Cell{ + name: name + inputs: inputs.clone() + compute: compute + callbacks: map[string]Callback{} + } + r.settle() or { return err } +} + +// set_value gives the input cell called `name` a new value and lets the cells +// depending on it follow, so the reaction settles before this returns. The +// callbacks of the cells whose value really changed are called exactly once, +// with the value those cells hold in the new stable state. +// +// > returns an error when there is no cell called `name`, or when it is a +// > compute cell, whose value is derived rather than set +fn (mut r Reaction) set_value(name string, value int) ! { + mut cell := r.cells[name] or { return error('there is no cell called ${name}') } + if !cell.is_input { + return error('the value of the compute cell ${name} cannot be set') + } + before := r.snapshot() + cell.value = value + r.cells[name] = cell + r.settle() or { return err } + r.report(before) +} + +// add_callback registers `callback` under `id` for the cell called `name`, so it +// is called with the new value of that cell whenever the value changes. A +// second callback under the same `id` replaces the first one. +// +// > returns an error when there is no cell called `name` +fn (mut r Reaction) add_callback(name string, id string, callback Callback) ! { + mut cell := r.cells[name] or { return error('there is no cell called ${name}') } + cell.callbacks[id] = callback + r.cells[name] = cell +} + +// remove_callback drops the callback registered under `id` for the cell called +// `name`. Removing an id that is not registered changes nothing. +// +// > returns an error when there is no cell called `name` +fn (mut r Reaction) remove_callback(name string, id string) ! { + mut cell := r.cells[name] or { return error('there is no cell called ${name}') } + cell.callbacks.delete(id) + r.cells[name] = cell +} + +// value returns the value the cell called `name` holds in the current stable +// state. +// +// > returns an error when there is no cell called `name` +fn (r &Reaction) value(name string) !int { + cell := r.cells[name] or { return error('there is no cell called ${name}') } + return cell.value +} + +// snapshot returns the value every cell holds right now. +fn (r &Reaction) snapshot() map[string]int { + mut values := map[string]int{} + for name in r.cell_names() { + values[name] = r.current_value(name) + } + return values +} + +// report calls the callbacks of every cell whose value differs from the value it +// had in the snapshot `before`, and of no other cell. Each callback of a cell is +// called once, however many inputs of that cell changed. +fn (mut r Reaction) report(before map[string]int) { + for name in r.cell_names() { + mut cell := r.cells[name] or { continue } + if cell.value == before[name] { + continue + } + mut ids := cell.callbacks.keys() + ids.sort() + for id in ids { + callback := cell.callbacks[id] or { continue } + callback(cell.value) + } + } +} + +// settle recalculates the compute cells until none of them changes any more. +// Cells that do not depend on each other in a cycle reach one single stable +// state, whatever order they are visited in, so repeating the pass until nothing +// moves is enough to find that state. +// +// > returns an error when the cells depend on each other in a cycle, which +// > leaves the reaction without a stable state +fn (mut r Reaction) settle() ! { + names := r.cell_names() + mut passes_left := names.len * names.len + 1 + for passes_left > 0 { + mut changed := false + for name in names { + mut cell := r.cells[name] or { continue } + if cell.is_input { + continue + } + compute := cell.compute or { + return error('the compute cell ${name} has no formula') + } + next := compute(r.input_values(cell.inputs)) + if next != cell.value { + cell.value = next + r.cells[name] = cell + changed = true + } + } + if !changed { + return + } + passes_left-- + } + return error('the cells of this reaction depend on each other in a cycle') +} + +// input_values collects the value every named cell currently holds. +fn (r &Reaction) input_values(names []string) []int { + mut values := []int{} + for name in names { + values << r.current_value(name) + } + return values +} + +// current_value returns the value the named cell holds. +fn (r &Reaction) current_value(name string) int { + cell := r.cells[name] or { Cell{} } + return cell.value +} + +// cell_names returns the names of every cell, sorted, so that the reaction +// always walks its cells in the same order. +fn (r &Reaction) cell_names() []string { + mut names := r.cells.keys() + names.sort() + return names +} diff --git a/exercises/practice/react/.meta/tests.toml b/exercises/practice/react/.meta/tests.toml new file mode 100644 index 0000000000..9b29a0a0ae --- /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.v b/exercises/practice/react/react.v new file mode 100644 index 0000000000..41d4c79dcc --- /dev/null +++ b/exercises/practice/react/react.v @@ -0,0 +1,80 @@ +module main + +// A Callback is called with the new value of a cell whenever the value of that +// cell in a new stable state differs from the value it had in the previous one. +type Callback = fn (int) + +// A Compute derives the value of a compute cell from the values of the cells it +// depends on, given in the order those cells were registered. +type Compute = fn ([]int) int + +// One cell of a reaction. An input cell only holds a value, a compute cell also +// holds the formula that derives that value from other cells. +struct Cell { +mut: + name string + is_input bool + inputs []string + compute ?Compute + value int + callbacks map[string]Callback +} + +// A Reaction is a set of named cells. Giving an input cell a new value makes +// every cell that depends on it follow along, so the reaction always ends up in +// a new stable state before a change is reported. +struct Reaction { +mut: + cells map[string]Cell +} + +// new_reaction returns a reaction that has no cells yet. +fn new_reaction() &Reaction { +} + +// create_input adds an input cell called `name` that holds `initial_value`. +// +// > returns an error when a cell called `name` is there already +fn (mut r Reaction) create_input(name string, initial_value int) ! { +} + +// create_compute adds a compute cell called `name`. Its value is `compute` +// applied to the values of `inputs`, in the order they are given, and it is +// calculated as soon as the cell is added so it holds a value right away. +// +// > returns an error when a cell called `name` is there already, or when one of +// > `inputs` is not a cell of this reaction +fn (mut r Reaction) create_compute(name string, inputs []string, compute Compute) ! { +} + +// set_value gives the input cell called `name` a new value and lets the cells +// depending on it follow, so the reaction settles before this returns. The +// callbacks of the cells whose value really changed are called exactly once, +// with the value those cells hold in the new stable state. +// +// > returns an error when there is no cell called `name`, or when it is a +// > compute cell, whose value is derived rather than set +fn (mut r Reaction) set_value(name string, value int) ! { +} + +// add_callback registers `callback` under `id` for the cell called `name`, so it +// is called with the new value of that cell whenever the value changes. A +// second callback under the same `id` replaces the first one. +// +// > returns an error when there is no cell called `name` +fn (mut r Reaction) add_callback(name string, id string, callback Callback) ! { +} + +// remove_callback drops the callback registered under `id` for the cell called +// `name`. Removing an id that is not registered changes nothing. +// +// > returns an error when there is no cell called `name` +fn (mut r Reaction) remove_callback(name string, id string) ! { +} + +// value returns the value the cell called `name` holds in the current stable +// state. +// +// > returns an error when there is no cell called `name` +fn (r &Reaction) value(name string) !int { +} diff --git a/exercises/practice/react/run_test.v b/exercises/practice/react/run_test.v new file mode 100644 index 0000000000..5be21bdcd7 --- /dev/null +++ b/exercises/practice/react/run_test.v @@ -0,0 +1,225 @@ +module main + +// Recorder collects the values a callback was called with, in the order the cell +// reported them. The struct is marked as heap allocated so that its `record` +// method can be used as a value, which is how the callback below is built. +@[heap] +struct Recorder { +mut: + values []int +} + +fn (mut r Recorder) record(value int) { + r.values << value +} + +// new_callback returns a recorder to read back what a callback was called with, +// together with the callback itself. The callback is a method value of the +// recorder, so the values it records stay visible to the test that asked for +// the callback. +fn new_callback() (&Recorder, fn (int)) { + recorder := &Recorder{ + values: []int{} + } + return recorder, recorder.record +} + +fn test_input_cells_have_a_value() { + mut reaction := new_reaction() + reaction.create_input('input', 10)! + assert reaction.value('input')! == 10 +} + +fn test_an_input_cells_value_can_be_set() { + mut reaction := new_reaction() + reaction.create_input('input', 4)! + reaction.set_value('input', 20)! + assert reaction.value('input')! == 20 +} + +fn test_compute_cells_calculate_initial_value() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('output', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + assert reaction.value('output')! == 2 +} + +fn test_compute_cells_take_inputs_in_the_right_order() { + mut reaction := new_reaction() + reaction.create_input('one', 1)! + reaction.create_input('two', 2)! + reaction.create_compute('output', ['one', 'two'], fn (inputs []int) int { + return inputs[0] + inputs[1] * 10 + })! + assert reaction.value('output')! == 21 +} + +fn test_compute_cells_update_value_when_dependencies_are_changed() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('output', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + reaction.set_value('input', 3)! + assert reaction.value('output')! == 4 +} + +fn test_compute_cells_can_depend_on_other_compute_cells() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('times_two', ['input'], fn (inputs []int) int { + return inputs[0] * 2 + })! + reaction.create_compute('times_thirty', ['input'], fn (inputs []int) int { + return inputs[0] * 30 + })! + sum := fn (inputs []int) int { + return inputs[0] + inputs[1] + } + reaction.create_compute('output', ['times_two', 'times_thirty'], sum)! + assert reaction.value('output')! == 32 + reaction.set_value('input', 3)! + assert reaction.value('output')! == 96 +} + +fn test_compute_cells_fire_callbacks() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('output', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + callback1, notify := new_callback() + reaction.add_callback('output', 'callback1', notify)! + reaction.set_value('input', 3)! + assert callback1.values == [4] +} + +fn test_callback_cells_only_fire_on_change() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('output', ['input'], fn (inputs []int) int { + return if inputs[0] < 3 { 111 } else { 222 } + })! + callback1, notify := new_callback() + reaction.add_callback('output', 'callback1', notify)! + reaction.set_value('input', 2)! + assert callback1.values.len == 0 + reaction.set_value('input', 4)! + assert callback1.values == [222] +} + +fn test_callbacks_do_not_report_already_reported_values() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('output', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + callback1, notify := new_callback() + reaction.add_callback('output', 'callback1', notify)! + reaction.set_value('input', 2)! + assert callback1.values == [3] + reaction.set_value('input', 3)! + assert callback1.values == [3, 4] +} + +fn test_callbacks_can_fire_from_multiple_cells() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('plus_one', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + reaction.create_compute('minus_one', ['input'], fn (inputs []int) int { + return inputs[0] - 1 + })! + callback1, notify1 := new_callback() + callback2, notify2 := new_callback() + reaction.add_callback('plus_one', 'callback1', notify1)! + reaction.add_callback('minus_one', 'callback2', notify2)! + reaction.set_value('input', 10)! + assert callback1.values == [11] + assert callback2.values == [9] +} + +fn test_callbacks_can_be_added_and_removed() { + mut reaction := new_reaction() + reaction.create_input('input', 11)! + reaction.create_compute('output', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + callback1, notify1 := new_callback() + callback2, notify2 := new_callback() + callback3, notify3 := new_callback() + reaction.add_callback('output', 'callback1', notify1)! + reaction.add_callback('output', 'callback2', notify2)! + reaction.set_value('input', 31)! + assert callback1.values == [32] + assert callback2.values == [32] + reaction.remove_callback('output', 'callback1')! + reaction.add_callback('output', 'callback3', notify3)! + reaction.set_value('input', 41)! + assert callback1.values == [32] + assert callback2.values == [32, 42] + assert callback3.values == [42] +} + +fn test_removing_a_callback_multiple_times_doesnt_interfere_with_other_callbacks() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('output', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + callback1, notify1 := new_callback() + callback2, notify2 := new_callback() + reaction.add_callback('output', 'callback1', notify1)! + reaction.add_callback('output', 'callback2', notify2)! + reaction.remove_callback('output', 'callback1')! + reaction.remove_callback('output', 'callback1')! + reaction.remove_callback('output', 'callback1')! + reaction.set_value('input', 2)! + assert callback1.values.len == 0 + assert callback2.values == [3] +} + +fn test_callbacks_should_only_be_called_once_even_if_multiple_dependencies_change() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('plus_one', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + reaction.create_compute('minus_one1', ['input'], fn (inputs []int) int { + return inputs[0] - 1 + })! + reaction.create_compute('minus_one2', ['minus_one1'], fn (inputs []int) int { + return inputs[0] - 1 + })! + reaction.create_compute('output', ['plus_one', 'minus_one2'], fn (inputs []int) int { + return inputs[0] * inputs[1] + })! + callback1, notify := new_callback() + reaction.add_callback('output', 'callback1', notify)! + reaction.set_value('input', 4)! + assert callback1.values == [10] +} + +fn test_callbacks_should_not_be_called_if_dependencies_change_but_output_value_doesnt_change() { + mut reaction := new_reaction() + reaction.create_input('input', 1)! + reaction.create_compute('plus_one', ['input'], fn (inputs []int) int { + return inputs[0] + 1 + })! + reaction.create_compute('minus_one', ['input'], fn (inputs []int) int { + return inputs[0] - 1 + })! + reaction.create_compute('always_two', ['plus_one', 'minus_one'], fn (inputs []int) int { + return inputs[0] - inputs[1] + })! + callback1, notify := new_callback() + reaction.add_callback('always_two', 'callback1', notify)! + for value in [2, 3, 4, 5] { + reaction.set_value('input', value)! + assert callback1.values.len == 0 + } + assert reaction.value('always_two')! == 2 +}