import test from 'node:test' import assert from 'node:assert/strict' import { pure, effect, map, flatMap, sequence, draw, randomSeed, drawScene, trace } from '../src/io.js' import { freeze as deepFreeze } from './helpers.js' const r1 = deepFreeze({ tag: 'Rect', x: 0, y: 0, w: 1, h: 1, color: '#000000' }) const t1 = deepFreeze({ tag: 'Text', x: 1, y: 2, text: 'hi', color: '#ffffff', size: 16, align: 'left' }) const drawOp = (shape) => ({ tag: 'Draw', shape }) const seedOp = { tag: 'RandomSeed' } test('m07 trace of pure(3) has result 3 and no ops', () => assert.deepStrictEqual(trace(pure(3), 0), { result: 3, ops: [] })) test('m07 randomSeed is answered with the seed given to trace', () => assert.equal(trace(randomSeed, 9).result, 9)) test('m07 randomSeed records a RandomSeed op', () => assert.deepStrictEqual(trace(randomSeed, 9).ops, [seedOp])) test('m07 effect(op) is an Effect whose continuation is pure', () => { const io = effect(seedOp) assert.equal(io.tag, 'Effect') assert.deepStrictEqual(io.op, seedOp) assert.deepStrictEqual(io.k(5), pure(5)) }) test('m07 draw records a Draw op and produces undefined', () => assert.deepStrictEqual(trace(draw(r1), 0), { result: undefined, ops: [drawOp(r1)] })) test('m07 drawScene draws the shapes in order', () => assert.deepStrictEqual(trace(drawScene([r1, t1]), 0).ops, [drawOp(r1), drawOp(t1)])) test('m07 drawScene of an empty scene has no ops', () => assert.deepStrictEqual(trace(drawScene([]), 0).ops, [])) test('m07 sequence of nothing has no ops', () => assert.deepStrictEqual(trace(sequence([]), 0), { result: undefined, ops: [] })) test('m07 sequence runs effects left to right', () => assert.deepStrictEqual(trace(sequence([randomSeed, draw(t1), draw(r1)]), 0).ops, [seedOp, drawOp(t1), drawOp(r1)])) test('m07 map transforms the result without adding ops', () => assert.deepStrictEqual(trace(map(randomSeed, (s) => s + 1), 41), { result: 42, ops: [seedOp] })) test('m07 flatMap passes the result of the first IO to the second', () => assert.deepStrictEqual(trace(flatMap(randomSeed, (s) => draw({ ...r1, x: s })), 7).ops, [seedOp, drawOp({ ...r1, x: 7 })])) test('m07 building map(io, throwingF) does not throw', () => { const boom = () => { throw new Error('boom') } assert.doesNotThrow(() => map(randomSeed, boom)) }) test('m07 tracing map(io, throwingF) runs f and throws', () => { const boom = () => { throw new Error('boom') } assert.throws(() => trace(map(randomSeed, boom), 0), /boom/) }) // Monad laws, checked by interpreting both sides and comparing {result, ops}. const programs = { 'a pure value': () => pure(4), 'randomSeed': () => randomSeed, 'a draw followed by reading the seed': () => flatMap(draw(r1), () => randomSeed), } const f = (n) => flatMap(draw({ ...r1, x: n }), () => pure(n * 2)) const g = (n) => map(randomSeed, (s) => s + n) Object.entries(programs).forEach(([name, program]) => { test(`m07 left identity holds for ${name}`, () => { const a = trace(program(), 5).result assert.deepStrictEqual(trace(flatMap(pure(a), f), 5), trace(f(a), 5)) }) test(`m07 right identity holds for ${name}`, () => assert.deepStrictEqual(trace(flatMap(program(), pure), 5), trace(program(), 5))) test(`m07 associativity holds for ${name}`, () => assert.deepStrictEqual( trace(flatMap(flatMap(program(), f), g), 5), trace(flatMap(program(), (x) => flatMap(f(x), g)), 5))) })