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 offlatmap
- 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
- I feel
- translated to Python from the book "Functional Programming in Scala"
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.
@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