grscheller.fp.state

Module fp.state - state monad

Handling state functionally.

Pure FP State handling type:

  • class State: A pure FP immutable implementation for the State Monad
    • translated to Python from the book "Functional Programming in Scala"
      • authors Chiusana & Bjarnason
    • using bind instead of flatmap
      • I feel flatmap is misleading for non-container-like monads
      • flatmap name too long
        • without do-notation code tends to march to the right
        • bind for state monad is part of the user API
          • shorter to type
          • less of just an implementation detail
  1# Copyright 2024-2025 Geoffrey R. Scheller
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""### Module fp.state - state monad
 16
 17Handling state functionally.
 18
 19#### Pure FP State handling type:
 20
 21* class **State**: A pure FP immutable implementation for the State Monad
 22  * translated to Python from the book "Functional Programming in Scala"
 23    * authors Chiusana & Bjarnason
 24  * using `bind` instead of `flatmap`
 25    * I feel `flatmap` is misleading for non-container-like monads
 26    * flatmap name too long
 27      * without do-notation code tends to march to the right
 28      * `bind` for state monad is part of the user API
 29        * shorter to type
 30        * less of just an implementation detail
 31
 32"""
 33from __future__ import annotations
 34
 35__all__ = [ 'State' ]
 36
 37from collections.abc import Callable
 38from typing import Any, Never
 39
 40class State[S, A]():
 41    """Data structure generating values while propagating changes of state.
 42
 43    * class `State` represents neither a state nor (value, state) pair
 44      * it wraps a transformation old_state -> (value, new_state)
 45      * the `run` method is this wrapped transformation
 46    * `bind` is just state propagating function composition
 47      * `bind` is sometimes called "flatmap"
 48
 49    """
 50    __slots__ = 'run'
 51
 52    def __init__(self, run: Callable[[S], tuple[A, S]]) -> None:
 53        self.run = run
 54
 55    def bind[B](self, g: Callable[[A], State[S, B]]) -> State[S, B]:
 56        def compose(s: S) -> tuple[B, S]:
 57            a, s1 = self.run(s)
 58            return g(a).run(s1)
 59        return State(lambda s: compose(s))
 60
 61    def map[B](self, f: Callable[[A], B]) -> State[S, B]:
 62        return self.bind(lambda a: State.unit(f(a)))
 63
 64    def map2[B, C](self, sb: State[S, B], f: Callable[[A, B], C]) -> State[S, C]:
 65        return self.bind(lambda a: sb.map(lambda b: f(a, b)))
 66
 67    def both[B](self, rb: State[S, B]) -> State[S, tuple[A, B]]:
 68        return self.map2(rb, lambda a, b: (a, b))
 69
 70    @staticmethod
 71    def unit[S1, B](b: B) -> State[S1, B]:
 72        """Create a State action from a value."""
 73        return State(lambda s: (b, s))
 74
 75    @staticmethod
 76    def getState[S1]() -> State[S1, S1]:
 77        """Set run action to return the current state
 78
 79        * the current state is propagated unchanged
 80        * current value now set to current state
 81
 82        """
 83        return State[S1, S1](lambda s: (s, s))
 84
 85    @staticmethod
 86    def setState[S1](s: S1) -> State[S1, tuple[()]]:
 87        """Manually set a state.
 88
 89        * the run action
 90          * ignores previous state and swaps in a new state
 91          * assigns a canonically meaningless value to current value
 92
 93        """
 94        return State(lambda _: ((), s))
 95
 96    @staticmethod
 97    def modifyState[S1](f: Callable[[S1], S1]) -> State[S1, tuple[()]]:
 98        return State.getState().bind(lambda a: State.setState(f(a)))  #type: ignore
 99
100 #   @staticmethod
101 #   def sequence[S1, A1](sas: list[State[S1, A1]])
102 #       """Combine a list of state actions into a state action of a list.
103
104 #       * all state actions must be of the same type
105
106 #       """
class State(typing.Generic[S, A]):
41class State[S, A]():
42    """Data structure generating values while propagating changes of state.
43
44    * class `State` represents neither a state nor (value, state) pair
45      * it wraps a transformation old_state -> (value, new_state)
46      * the `run` method is this wrapped transformation
47    * `bind` is just state propagating function composition
48      * `bind` is sometimes called "flatmap"
49
50    """
51    __slots__ = 'run'
52
53    def __init__(self, run: Callable[[S], tuple[A, S]]) -> None:
54        self.run = run
55
56    def bind[B](self, g: Callable[[A], State[S, B]]) -> State[S, B]:
57        def compose(s: S) -> tuple[B, S]:
58            a, s1 = self.run(s)
59            return g(a).run(s1)
60        return State(lambda s: compose(s))
61
62    def map[B](self, f: Callable[[A], B]) -> State[S, B]:
63        return self.bind(lambda a: State.unit(f(a)))
64
65    def map2[B, C](self, sb: State[S, B], f: Callable[[A, B], C]) -> State[S, C]:
66        return self.bind(lambda a: sb.map(lambda b: f(a, b)))
67
68    def both[B](self, rb: State[S, B]) -> State[S, tuple[A, B]]:
69        return self.map2(rb, lambda a, b: (a, b))
70
71    @staticmethod
72    def unit[S1, B](b: B) -> State[S1, B]:
73        """Create a State action from a value."""
74        return State(lambda s: (b, s))
75
76    @staticmethod
77    def getState[S1]() -> State[S1, S1]:
78        """Set run action to return the current state
79
80        * the current state is propagated unchanged
81        * current value now set to current state
82
83        """
84        return State[S1, S1](lambda s: (s, s))
85
86    @staticmethod
87    def setState[S1](s: S1) -> State[S1, tuple[()]]:
88        """Manually set a state.
89
90        * the run action
91          * ignores previous state and swaps in a new state
92          * assigns a canonically meaningless value to current value
93
94        """
95        return State(lambda _: ((), s))
96
97    @staticmethod
98    def modifyState[S1](f: Callable[[S1], S1]) -> State[S1, tuple[()]]:
99        return State.getState().bind(lambda a: State.setState(f(a)))  #type: ignore

Data structure generating values while propagating changes of state.

  • class State represents neither a state nor (value, state) pair
    • it wraps a transformation old_state -> (value, new_state)
    • the run method is this wrapped transformation
  • bind is just state propagating function composition
    • bind is sometimes called "flatmap"
State(run: 'Callable[[S], tuple[A, S]]')
53    def __init__(self, run: Callable[[S], tuple[A, S]]) -> None:
54        self.run = run
run
def bind(self, g: 'Callable[[A], State[S, B]]') -> 'State[S, B]':
56    def bind[B](self, g: Callable[[A], State[S, B]]) -> State[S, B]:
57        def compose(s: S) -> tuple[B, S]:
58            a, s1 = self.run(s)
59            return g(a).run(s1)
60        return State(lambda s: compose(s))
def map(self, f: 'Callable[[A], B]') -> 'State[S, B]':
62    def map[B](self, f: Callable[[A], B]) -> State[S, B]:
63        return self.bind(lambda a: State.unit(f(a)))
def map2(self, sb: 'State[S, B]', f: 'Callable[[A, B], C]') -> 'State[S, C]':
65    def map2[B, C](self, sb: State[S, B], f: Callable[[A, B], C]) -> State[S, C]:
66        return self.bind(lambda a: sb.map(lambda b: f(a, b)))
def both(self, rb: 'State[S, B]') -> 'State[S, tuple[A, B]]':
68    def both[B](self, rb: State[S, B]) -> State[S, tuple[A, B]]:
69        return self.map2(rb, lambda a, b: (a, b))
@staticmethod
def unit(b: 'B') -> 'State[S1, B]':
71    @staticmethod
72    def unit[S1, B](b: B) -> State[S1, B]:
73        """Create a State action from a value."""
74        return State(lambda s: (b, s))

Create a State action from a value.

@staticmethod
def getState() -> 'State[S1, S1]':
76    @staticmethod
77    def getState[S1]() -> State[S1, S1]:
78        """Set run action to return the current state
79
80        * the current state is propagated unchanged
81        * current value now set to current state
82
83        """
84        return State[S1, S1](lambda s: (s, s))

Set run action to return the current state

  • the current state is propagated unchanged
  • current value now set to current state
@staticmethod
def setState(s: 'S1') -> 'State[S1, tuple[()]]':
86    @staticmethod
87    def setState[S1](s: S1) -> State[S1, tuple[()]]:
88        """Manually set a state.
89
90        * the run action
91          * ignores previous state and swaps in a new state
92          * assigns a canonically meaningless value to current value
93
94        """
95        return State(lambda _: ((), s))

Manually set a state.

  • the run action
    • ignores previous state and swaps in a new state
    • assigns a canonically meaningless value to current value
@staticmethod
def modifyState(f: 'Callable[[S1], S1]') -> 'State[S1, tuple[()]]':
97    @staticmethod
98    def modifyState[S1](f: Callable[[S1], S1]) -> State[S1, tuple[()]]:
99        return State.getState().bind(lambda a: State.setState(f(a)))  #type: ignore