module Snake.Tests.M04State open Expecto open Snake open Snake.State // Small initial states for the "behaves like" checks: two States behave alike when they give the same pair from each. let private starts = [ 0; 1; 7; 42 ] let private sameOnAll (st1: State) (st2: State) : unit = for s in starts do Expect.equal (State.run s st1) (State.run s st2) $"from state {s}" // A counter step: gives the current number times 10 and adds 1 to the state. A function of unit, so that a stub // that throws fails each test on its own instead of the whole file at load time. let private step () : State = State.get () |> State.bind (fun n -> State.put (n + 1) |> State.map (fun () -> n * 10)) let private f (a: int) : State = State.modify (fun s -> s * 2) |> State.map (fun () -> a + 1) let private g (b: int) : State = State.get () |> State.bind (fun s -> State.pure (b, s)) [] let tests = testList "M04 State" [ testCase "State pure gives its value and leaves the state alone" (fun () -> Expect.equal (State.run 99 (State.pure "x")) ("x", 99) "pure") testCase "State map applies f to the value and keeps the state from the underlying step" (fun () -> Expect.equal (State.run 4 (State.map (fun v -> v + 1) (step ()))) (41, 5) "map") testCase "State bind threads the state through both steps" (fun () -> let both = step () |> State.bind (fun a -> step () |> State.map (fun b -> (a, b))) Expect.equal (State.run 4 both) ((40, 50), 6) "bind") testCase "State get gives the state as the value and leaves it alone" (fun () -> Expect.equal (State.run "board" (State.get ())) ("board", "board") "get") testCase "State put replaces the state and gives unit" (fun () -> Expect.equal (State.run 99 (State.put 5)) ((), 5) "put") testCase "State modify applies f to the state and gives unit" (fun () -> Expect.equal (State.run 21 (State.modify (fun s -> s * 2))) ((), 42) "modify") testCase "State law: left identity, bind f (pure a) behaves like f a" (fun () -> sameOnAll (State.pure 3 |> State.bind f) (f 3)) testCase "State law: right identity, bind pure m behaves like m" (fun () -> sameOnAll (step () |> State.bind State.pure) (step ())) testCase "State law: associativity, the grouping of bind does not matter" (fun () -> sameOnAll (step () |> State.bind f |> State.bind g) (step () |> State.bind (fun a -> f a |> State.bind g))) testCase "State a counter hands out fresh labels in order" (fun () -> let fresh = State.get () |> State.bind (fun n -> State.put (n + 1) |> State.map (fun () -> $"L{n}")) let three = fresh |> State.bind (fun a -> fresh |> State.bind (fun b -> fresh |> State.map (fun c -> [ a; b; c ]))) Expect.equal (State.run 0 three) ([ "L0"; "L1"; "L2" ], 3) "from 0" Expect.equal (State.run 7 three) ([ "L7"; "L8"; "L9" ], 10) "from 7") testCase "State Random is State with the seed as S: the dice give the same values and seeds" (fun () -> // One draw written only with State's get and put: read the seed, compute, store the next seed. let draw: State = State.get () |> State.bind (fun seed -> let v, next = Random.run seed Random.nextUint32 State.put next |> State.map (fun () -> v)) // The same draw, by turning a Random step into a State by hand. let wrapped: State = State(fun seed -> Random.run seed Random.nextUint32) let d6State = draw |> State.map (fun v -> 1u + v % 6u) let twoDiceState = d6State |> State.bind (fun a -> d6State |> State.map (fun b -> a + b)) let d6Random = Random.nextUint32 |> Random.map (fun v -> 1u + v % 6u) let twoDiceRandom = d6Random |> Random.bind (fun a -> d6Random |> Random.map (fun b -> a + b)) for seed in [ 0u; 1u; 42u; 4294967295u ] do Expect.equal (State.run seed draw) (Random.run seed Random.nextUint32) $"draw from {seed}" Expect.equal (State.run seed wrapped) (Random.run seed Random.nextUint32) $"wrapped from {seed}" Expect.equal (State.run seed d6State) (Random.run seed d6Random) $"d6 from {seed}" Expect.equal (State.run seed twoDiceState) (Random.run seed twoDiceRandom) $"twoDice from {seed}") ]