When programming, I often want to "share state with multiple functions".
At this time, it is better to choose the best from various solutions than to use global variables or bucket relay the pointer without thinking.
Therefore, I will summarize the method of sharing the state with multiple functions, along with the advantages and disadvantages, as much as I can think of.
Use Python as the sample code, and use Racket (a type of Scheme) when you can't write in Python. because I like.
state = "initial state"
def fuga0():
#Change after referring to the state
global state
print(f"fuga0: {state}")
state = "another state"
return "result of fuga0"
def fuga1():
#Refer to the state
print(f"fuga1: {state}")
def hoge():
print(fuga0())
fuga1()
print(f"last: {state}")
hoge()
Result (the following is omitted)
fuga0: initial state
result of fuga0
fuga1: another state
last: another state
Not a very recommended writing style.
from dataclasses import dataclass
from typing import Any
#A pointer to which this guy is bucket relayed
@dataclass
class Box:
value: Any
def fuga0(box):
print(f"fuga0: {box.value}")
box.value = "another state"
return "result of fuga0"
def fuga1(box):
print(f"fuga1: {box.value}")
def hoge(box):
print(fuga0(box))
fuga1(box)
print(f"last: {box.value}")
b = Box("initial state")
hoge(b)
In the case of a constant, the value should be bucket-relayed instead of the pointer.
It is a so-called functional writing style, and the state does not change in the code.
def fuga0(state):
print(f"fuga0: {state}")
return "result of fuga0", "another state"
def fuga1(state):
print(f"fuga1: {state}")
def hoge(state):
result, new_state = fuga0(state)
print(result)
fuga1(new_state)
print(f"last: {new_state}")
hoge("initial")
A common practice in object-oriented languages. Make the state you want to share a member variable.
class Hoge:
def __init__(self):
self._state = "initial state"
def _fuga0(self):
print(f"fuga0: {self._state}")
self._state = "another state"
return "result of fuga0"
def _fuga1(self):
print(f"fuga0: {self._state}")
def __call__(self):
print(self._fuga0())
self._fuga1()
print(f"last: {self._state}")
hoge = Hoge()
hoge()
Close to object-oriented.
def create_hoge():
state = "initial"
def fuga0():
nonlocal state
print(f"fuga0: {state}")
state = "another state"
return "result of fuga0"
def fuga1():
print(f"fuga1: {state}")
def hoge():
print(fuga0())
fuga1()
print(f"last: {state}")
return hoge
create_hoge()()
nonlocal
and you can write easily.Glocal Variable
Pattern of my thoughts. Create a variable that can be used only in with. See the link for details.
param.py
_param = None
_initialized = False
@contextmanager
def parametrize(data):
global _param
global _initialized
before = _param
before_initialized = _initialized
_param = data
_initialized = True
try:
yield
finally:
_param = before
_initialized = before_initialized
def set_param(data):
if _initialized:
global _param
_param = data
else:
raise RuntimeError
def get_param():
if _initialized:
return _param
else:
raise RuntimeError
from param import get_param, set_param, parametrize
def fuga0():
print(f"fuga0: {get_param()}")
set_param("another state")
return "result of fuga0"
def fuga1():
print(f"fuga0: {get_param()}")
def hoge():
print(fuga0())
fuga1()
print(f"last: {get_param()}")
with parametrize("initial state"):
hoge()
What is a monad? What is a state monad? Please google for something like that.
Let's write with Racket. I'm a little unsure about the implementation of applicative ...
;Definition of state monads from here
(require (prefix-in base: racket/base))
(require data/functor)
(require data/applicative)
(require data/monad)
(struct result (value state) #:transparent)
(struct state (f)
#:methods gen:functor
[(define (map g x)
(state (λ (s)
(match-define (result v ss) (run x s))
(result (g v) ss))))]
#:methods gen:applicative
[(define (pure _ x)
(state (λ (s) (result x s))))
(define (apply f xs)
(define (get-args xs s)
(match xs
[(cons x rest)
(match-define (result xv xs) (run x s))
(match-define (result args argss) (get-args rest xs))
(result (cons xv args) argss)]
[_ (result `() s)]))
(state (λ (s)
(match-define (result fv fs) (run f s))
(match-define (result args argss) (get-args xs fs))
(result (base:apply fv args) argss))))]
#:methods gen:monad
[(define (chain f x)
(state (λ (s)
(match-define (result xv xs) (run x s))
(match-define (result fv fs) (run (f xv) xs))
(result fv fs))))])
(define (run m s) ((state-f m) s))
(define get
(state (λ (s) (result s s))))
(define (set ns)
(state (λ (s) (result s ns))))
;Definition up to here
(define fuga0
(do [x <- get]
(pure (printf "fuga0: ~a\n" x))
(set "another state")
(pure "result of fuga0")))
(define fuga1
(do [x <- get]
(pure (printf "fuga1: ~a\n" x))))
(define hoge
(do [x <- fuga0]
(pure (displayln x))
fuga1
[y <- get]
(pure (printf "last: ~a\n" y))))
(run hoge "initial state")
If it's a constant, use the Reader monad.
It's quite a penance in Python, but if you do your best, it looks like this.
#State monad definition from here
from typing import Callable, TypeVar, Generic, Tuple
S = TypeVar("S") #State type
R = TypeVar("R") #Return type
A = TypeVar("A") #New return type
class State(Generic[S, R]):
def __init__(self, f: Callable[[S], Tuple[S, R]]):
self._f = f
def run(self, state: S) -> Tuple[S, R]:
return self._f(state)
@staticmethod
def of(value: R):
return State(lambda s: (value, s))
def flatmap(self, g: Callable[[R], State[S, A]]) -> State[S, A]:
def _new(state):
f_ret, f_state = self.run(state)
return g(f_ret).run(f_state)
return State(_new)
def map(self, g: Callable[[R], A]) -> State[S, A]:
return self.flatmap(lambda x: State.of(g(x)))
def then(self, m: State[S, A]) -> State[S, A]:
return self.flatmap(lambda _: m)
def value(self, v: A) -> State[S, A]:
return self.map(lambda _: v)
get_m = State(lambda s: (s, s))
def set_m(new_state):
return State(lambda s: (s, new_state))
#State monad definition so far
fuga0 = (get_m
.map(lambda v: print(f"fuga0: {v}"))
.then(set_m("another_state"))
.value("result of fuga0"))
fuga1 = (get_m
.map(lambda v: print(f"fuga1: {v}")))
hoge = (fuga0
.map(print)
.then(fuga1)
.then(get_m)
.map(lambda v: print(f"last: {v}")))
hoge.run("initial state")
What is limited continuation? So why can we handle state changes? Please refer to Asai-sensei's tutorial.
(require racket/control)
(define (get)
(shift k
(λ (x)
((k x) x))))
(define (set s)
(shift k
(λ (x)
((k x) s))))
(define (run m s)
((reset
(let ([ret (m)])
(λ (_) ret))) s))
(define (fuga0)
(printf "fuga0: ~a\n" (get))
(set "another state")
"result of fuga0")
(define (fuga1)
(printf "fuga1: ~a\n" (get)))
(define (hoge)
(displayln (fuga0))
(fuga1)
(printf "last: ~a\n" (get)))
(run hoge "initial state")
Everybody say everyone differently.
Instead of sticking to what programmers know, increase the number of tools and use them properly.
Also, there is contextvars in the Python standard library. I didn't mention it this time because it seems to have been created with asynchrony in mind.
Recommended Posts