Skip to content

Commit cfe69f0

Browse files
committed
Merge pull request #41 from radix/eref
add an ERef object, like Haskell's IORef.
2 parents 132710d + 9695d24 commit cfe69f0

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

effect/ref.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from characteristic import attributes
2+
3+
from ._base import Effect
4+
from ._dispatcher import TypeDispatcher
5+
from ._sync import sync_performer
6+
7+
8+
class Reference(object):
9+
"""
10+
An effectful mutable variable, suitable for sharing between multiple
11+
logical threads of execution, that can be read and modified in a purely
12+
functional way.
13+
14+
Compare to Haskell's ``IORef`` or Clojure's ``atom``.
15+
"""
16+
17+
# TODO: Add modify_atomic that either uses a lock or a low-level
18+
# compare-and-set operation.
19+
20+
def __init__(self, initial):
21+
self._value = initial
22+
23+
def read(self):
24+
"""Return an Effect that results in the current value."""
25+
return Effect(ReadReference(ref=self))
26+
27+
def modify(self, transformer):
28+
"""
29+
Return an Effect that updates the value with ``fn(old_value)``.
30+
31+
This is not guaranteed to be linearizable if multiple threads are
32+
modifying the reference at the same time.
33+
"""
34+
return Effect(ModifyReference(ref=self, transformer=transformer))
35+
36+
37+
@attributes(['ref'])
38+
class ReadReference(object):
39+
"""Intent that gets a Reference's current value."""
40+
41+
42+
@attributes(['ref', 'transformer'])
43+
class ModifyReference(object):
44+
"""
45+
Intent that modifies a Reference value in-place with a transformer func.
46+
47+
This intent is not necessarily linearizable if multiple threads are
48+
modifying the same reference at the same time.
49+
"""
50+
51+
52+
@sync_performer
53+
def perform_read_reference(dispatcher, intent):
54+
"""Performer for :obj:`ReadReference`."""
55+
return intent.ref._value
56+
57+
58+
@sync_performer
59+
def perform_modify_reference(dispatcher, intent):
60+
"""
61+
Performer for :obj:`ModifyReference`.
62+
63+
This performer is not linearizable if multiple physical threads are
64+
modifying the same reference at the same time.
65+
"""
66+
new_value = intent.transformer(intent.ref._value)
67+
intent.ref._value = new_value
68+
return new_value
69+
70+
71+
reference_dispatcher = TypeDispatcher({
72+
ReadReference: perform_read_reference,
73+
ModifyReference: perform_modify_reference})

effect/test_ref.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from testtools import TestCase
2+
3+
from ._base import Effect
4+
from ._sync import sync_perform
5+
from .ref import (
6+
Reference, ModifyReference, ReadReference,
7+
reference_dispatcher)
8+
9+
10+
class ReferenceTests(TestCase):
11+
"""Tests for :obj:`Reference`."""
12+
13+
def test_read(self):
14+
"""``read`` returns an Effect that represents the current value."""
15+
ref = Reference('initial')
16+
self.assertEqual(ref.read(), Effect(ReadReference(ref=ref)))
17+
18+
def test_modify(self):
19+
"""``modify`` returns an Effect that represents modification."""
20+
ref = Reference(0)
21+
transformer = lambda x: x + 1
22+
eff = ref.modify(transformer)
23+
self.assertEqual(eff,
24+
Effect(ModifyReference(ref=ref,
25+
transformer=transformer)))
26+
27+
def test_perform_read(self):
28+
"""Performing the reading results in the current value."""
29+
ref = Reference('initial')
30+
result = sync_perform(reference_dispatcher, ref.read())
31+
self.assertEqual(result, 'initial')
32+
33+
def test_perform_modify(self):
34+
"""
35+
Performing the modification results in transforming the current value,
36+
and also returns the new value.
37+
"""
38+
ref = Reference(0)
39+
transformer = lambda x: x + 1
40+
result = sync_perform(reference_dispatcher, ref.modify(transformer))
41+
self.assertEqual(result, 1)
42+
self.assertEqual(sync_perform(reference_dispatcher, ref.read()), 1)

0 commit comments

Comments
 (0)