import test from 'node:test' import assert from 'node:assert/strict' import { pure, map, flatMap, get, put, modify, run } from '../src/state.js' import * as Random from '../src/random.js' // Small initial states for the "behaves like" checks: two States behave alike when they give the same pair from each. const starts = [0, 1, 7, 42] const sameOnAll = (st1, st2) => starts.forEach((s) => assert.deepStrictEqual(run(st1, s), run(st2, s), `from state ${s}`)) // A counter step: gives the current number times 10 and adds 1 to the state. Built inside the tests, so that a stub // that throws fails each test on its own instead of the whole file at load time. const step = () => flatMap(get, (n) => map(put(n + 1), () => n * 10)) test('m04 State pure gives its value and leaves the state alone', () => assert.deepStrictEqual(run(pure('x'), 99), ['x', 99])) test('m04 State map applies f to the value and keeps the state from the underlying step', () => assert.deepStrictEqual(run(map(step(), (v) => v + 1), 4), [41, 5])) test('m04 State flatMap threads the state through both steps', () => assert.deepStrictEqual(run(flatMap(step(), (a) => map(step(), (b) => [a, b])), 4), [[40, 50], 6])) test('m04 State get gives the state as the value and leaves it alone', () => assert.deepStrictEqual(run(get, 'board'), ['board', 'board'])) test('m04 State put replaces the state and gives undefined', () => assert.deepStrictEqual(run(put(5), 99), [undefined, 5])) test('m04 State modify applies f to the state and gives undefined', () => assert.deepStrictEqual(run(modify((s) => s * 2), 21), [undefined, 42])) test('m04 State law: left identity, flatMap(pure(a), f) behaves like f(a)', () => { const f = (a) => map(modify((s) => s * 2), () => a + 1) sameOnAll(flatMap(pure(3), f), f(3)) }) test('m04 State law: right identity, flatMap(m, pure) behaves like m', () => sameOnAll(flatMap(step(), pure), step())) test('m04 State law: associativity, the grouping of flatMap does not matter', () => { const f = (a) => map(modify((s) => s * 2), () => a + 1) const g = (b) => flatMap(get, (s) => pure([b, s])) sameOnAll(flatMap(flatMap(step(), f), g), flatMap(step(), (a) => flatMap(f(a), g))) }) test('m04 State a counter hands out fresh labels in order', () => { const fresh = flatMap(get, (n) => map(put(n + 1), () => `L${n}`)) const three = flatMap(fresh, (a) => flatMap(fresh, (b) => map(fresh, (c) => [a, b, c]))) assert.deepStrictEqual(run(three, 0), [['L0', 'L1', 'L2'], 3]) assert.deepStrictEqual(run(three, 7), [['L7', 'L8', 'L9'], 10]) }) test('m04 State Random is State with the seed as S: the dice give the same values and seeds', () => { // One draw written only with State's get and put: read the seed, compute, store the next seed. const draw = flatMap(get, (seed) => { const [v, next] = Random.run(Random.nextUint32, seed) return map(put(next), () => v) }) const d6State = map(draw, (v) => 1 + (v % 6)) const twoDiceState = flatMap(d6State, (a) => map(d6State, (b) => a + b)) const d6Random = Random.map(Random.nextUint32, (v) => 1 + (v % 6)) const twoDiceRandom = Random.flatMap(d6Random, (a) => Random.map(d6Random, (b) => a + b)) ;[0, 1, 42, 4294967295].forEach((seed) => { assert.deepStrictEqual(run(draw, seed), Random.run(Random.nextUint32, seed), `draw from ${seed}`) assert.deepStrictEqual(run(d6State, seed), Random.run(d6Random, seed), `d6 from ${seed}`) assert.deepStrictEqual(run(twoDiceState, seed), Random.run(twoDiceRandom, seed), `twoDice from ${seed}`) }) })