Skip to content

Commit

Permalink
refactor: rename ref (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sune Debel authored Oct 14, 2021
1 parent 1322593 commit 46c4606
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 42 deletions.
28 changes: 14 additions & 14 deletions docs/effectful_but_side_effect_free.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,40 +593,40 @@ s: Schedule[HasRandom] = jitter(spaced(timedelta(seconds=2)))
```

### Purely Functional State
Mutating non-local state is a side-effect that we want to avoid when doing functional programming. This means that we need a mechanism for managing state as an effect. `pfun.ref` provides exactly this. `pfun.ref` works by mutating state only by calling `Effect` instances.
Mutating non-local state is a side-effect that we want to avoid when doing functional programming. This means that we need a mechanism for managing state as an effect. `pfun.state` provides exactly this. `pfun.state` works by mutating state only by calling `Effect` instances.

```python
from typing import Tuple, NoReturn

from pfun.ref import Ref
from pfun.state import State
from pfun.effect import Effect


ref: Ref[Tuple[int, ...]] = Ref(())
add_1: Effect[object, NoReturn, None] = ref.modify(lambda old: return old + (1,))
state: State[Tuple[int, ...]] = State(())
add_1: Effect[object, NoReturn, None] = state.modify(lambda old: return old + (1,))
# calling modify doesn't modify the state directly
assert ref.value == ()
assert state.value == ()
# The state is modified only when the effect is called
add_1.run(None)
assert ref.value == (1,)
assert state.value == (1,)
```
`pfun.ref.Ref` protects access to the state using an `asyncio.Lock`, meaning that updating the state can be done atomically with the following methods:
`pfun.state.State` protects access to the state using an `asyncio.Lock`, meaning that updating the state can be done atomically with the following methods:

- `Ref.get()` read the current value of the state
- `Ref.set(new_state)` update the state to `new_value` atomically, meaning no other effect can read the value of the state while the update is in progress. Note that if you first read the state using `Ref.get` and then set it with `Ref.set`, other effects may read the value in between which may lead to lost updates. _For this use case you should use `modify` or `try_modify`_
- `Ref.modify(update_function)` read and update the state with `update_function` atomically, meaning no other effect can read or write the state before the effect produced by `modify` returns
- `Ref.try_modify(update_function)` read and update the state with `update_function` atomically, if `update_funciton` succeeds. Success is signaled by the `update_function` by returning a `pfun.either.Right` instance, and error by returning a `pfun.either.Left` instance.
- `State.get()` read the current value of the state
- `State.set(new_state)` update the state to `new_value` atomically, meaning no other effect can read the value of the state while the update is in progress. Note that if you first read the state using `State.get` and then set it with `State.set`, other effects may read the value in between which may lead to lost updates. _For this use case you should use `modify` or `try_modify`_
- `State.modify(update_function)` read and update the state with `update_function` atomically, meaning no other effect can read or write the state before the effect produced by `modify` returns
- `State.try_modify(update_function)` read and update the state with `update_function` atomically, if `update_funciton` succeeds. Success is signaled by the `update_function` by returning a `pfun.either.Right` instance, and error by returning a `pfun.either.Left` instance.

`pfun.ref` can of course be combined with the module pattern:
`pfun.state` can of course be combined with the module pattern:
```python
from typing import Tuple, NoReturn, Protocol

from pfun.ref import Ref
from pfun.state import State
from pfun.effect import depend, Effect


class HasState(Protocol):
state: Ref[Tuple[int, ...]]
state: State[Tuple[int, ...]]


def set_state(state: Tuple[int, ...]) -> Effect[HasState, NoReturn, None]:
Expand Down
2 changes: 1 addition & 1 deletion docs/ref_api.md → docs/state_api.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
::: pfun.ref.Ref
::: pfun.state.State
selection:
members:
- __init__
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ nav:
- 'pfun.functions': functions_api.md
- 'pfun.effect': effect_api.md
- 'pfun.schedule': schedule_api.md
- 'pfun.ref': ref_api.md
- 'pfun.state': state_api.md
- 'pfun.console': console_api.md
- 'pfun.files': files_api.md
- 'pfun.clock': clock_api.md
Expand Down
2 changes: 1 addition & 1 deletion src/pfun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing_extensions import Protocol

from . import clock, console, files, logging, random, ref, subprocess # noqa
from . import clock, console, files, logging, random, state, subprocess # noqa
from .dict import Dict # noqa
from .effect import * # noqa
from .either import Either, Left, Right # noqa
Expand Down
32 changes: 16 additions & 16 deletions src/pfun/ref.py → src/pfun/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
E = TypeVar('E')


class Ref(Immutable, Generic[A], init=False):
class State(Immutable, Generic[A], init=False):
"""
Wraps a value that can be mutated as an `Effect`
"""
Expand Down Expand Up @@ -44,8 +44,8 @@ def get(self) -> Success[A]:
Get an `Effect` that reads the current state of the value
Example:
>>> ref = Ref('the state')
>>> ref.get().run(None)
>>> state = State('the state')
>>> state.get().run(None)
'the state'
Return:
Expand All @@ -58,18 +58,18 @@ async def f(_) -> Either[NoReturn, A]:
return from_callable(f)

def __repr__(self):
return f'Ref({repr(self.value)})'
return f'State({repr(self.value)})'

@add_method_repr
def put(self, value: A) -> Success[None]:
"""
Get an `Effect` that updates the current state of the value
Example:
>>> ref = Ref('initial state')
>>> ref.put('new state').run(None)
>>> state = State('initial state')
>>> state.put('new state').run(None)
None
>>> ref.value
>>> state.value
'new state'
Args:
Expand All @@ -89,14 +89,14 @@ async def f(_) -> Either[NoReturn, None]:
@add_method_repr
def modify(self, f: Callable[[A], A]) -> Success[None]:
"""
Modify the value wrapped by this `Ref` by \
Modify the value wrapped by this `State` by \
applying `f` in isolation
Example:
>>> ref = Ref([])
>>> ref.modify(lambda l: l + [1]).run(None)
>>> state = State([])
>>> state.modify(lambda l: l + [1]).run(None)
None
>>> ref.value
>>> state.value
[1]
Args:
Expand All @@ -123,14 +123,14 @@ def try_modify(self,
Example:
>>> from pfun.either import Left, Right
>>> ref = Ref('initial state')
>>> ref.try_modify(lambda _: Left('Whoops!')).either().run(None)
>>> state = State('initial state')
>>> state.try_modify(lambda _: Left('Whoops!')).either().run(None)
Left('Whoops!')
>>> ref.value
>>> state.value
'initial state'
>>> ref.try_modify(lambda _: Right('new state')).run(None)
>>> state.try_modify(lambda _: Right('new state')).run(None)
None
>>> ref.value
>>> state.value
'new state'
Args:
Expand Down
19 changes: 10 additions & 9 deletions tests/test_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from pfun import (DefaultModules, Dict, Immutable, List, clock, compose,
console, effect, either, files, http, identity, logging,
random, ref, schedule, sql, subprocess)
random, schedule, sql, state, subprocess)
from pfun.effect import Resource
from pfun.hypothesis_strategies import anything, effects, rights, unaries

Expand Down Expand Up @@ -217,12 +217,13 @@ async def g(s: str) -> either.Either[str, str]:
assert effect.from_callable(g).run('env') == 'env'

def test_memoize(self):
state = ref.Ref(())
e = state.modify(lambda t: t + ('modify was called', )
).discard_and_then(effect.success('result')).memoize()
s = state.State(())
e = s.modify(
lambda t: t + ('modify was called', )
).discard_and_then(effect.success('result')).memoize()
double_e = e.discard_and_then(e)
assert double_e.run(None) == 'result'
assert state.value == ('modify was called', )
assert s.value == ('modify was called', )

@settings(deadline=None)
@given(effects(anything()), effects(anything()))
Expand Down Expand Up @@ -583,21 +584,21 @@ def test_append_bytes(self):

class TestRef:
def test_get(self):
int_ref = ref.Ref(0)
int_ref = state.State(0)
assert int_ref.get().run(None) == 0

def test_put(self):
int_ref = ref.Ref(0)
int_ref = state.State(0)
int_ref.put(1).run(None)
assert int_ref.value == 1

def test_modify(self):
int_ref = ref.Ref(0)
int_ref = state.State(0)
int_ref.modify(lambda _: 1).run(None)
assert int_ref.value == 1

def test_try_modify(self):
int_ref = ref.Ref(0)
int_ref = state.State(0)
int_ref.try_modify(lambda _: either.Left('')).either().run(None)
assert int_ref.value == 0
int_ref.try_modify(lambda _: either.Right(1)).run(None)
Expand Down

0 comments on commit 46c4606

Please sign in to comment.