Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions exercises/practice/react/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions exercises/practice/react/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"authors": [
"metif12"
],
"files": {
"solution": [
"react.v"
],
"test": [
"run_test.v"
],
"example": [
".meta/example.v"
]
},
"blurb": "Implement a basic reactive system."
}
209 changes: 209 additions & 0 deletions exercises/practice/react/.meta/example.v
Original file line number Diff line number Diff line change
@@ -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
}
52 changes: 52 additions & 0 deletions exercises/practice/react/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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"
80 changes: 80 additions & 0 deletions exercises/practice/react/react.v
Original file line number Diff line number Diff line change
@@ -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 {
}
Loading