Skip to content

Commit bb9518a

Browse files
Refactor ExpressionTuple type
* ExpressionTuple now makes constructive use of __slots__ * ExpressionTuple type is a tuple wrapper and no longer a tuple subclass * Improved str, repr, and pprint with tests * Explicit etuple unification tests
1 parent 8f98111 commit bb9518a

File tree

7 files changed

+236
-92
lines changed

7 files changed

+236
-92
lines changed

symbolic_pymc/etuple.py

Lines changed: 137 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import toolz
55

6+
from collections import Sequence
7+
68
from multipledispatch import dispatch
79

810
from kanren.term import operator, arguments
@@ -13,64 +15,108 @@
1315
etuple_repr.maxother = 100
1416

1517

16-
class KwdPair(tuple):
18+
class KwdPair(object):
1719
"""A class used to indicate a keyword + value mapping.
1820
1921
TODO: Could subclass `ast.keyword`.
2022
2123
"""
2224

23-
def __new__(cls, arg, value):
25+
__slots__ = ("arg", "value")
26+
27+
def __init__(self, arg, value):
2428
assert isinstance(arg, str)
25-
obj = super().__new__(cls, (arg, value))
26-
return obj
29+
self.arg = arg
30+
self.value = value
2731

2832
@property
2933
def eval_obj(self):
30-
return KwdPair(self[0], getattr(self[1], "eval_obj", self[1]))
34+
return KwdPair(self.arg, getattr(self.value, "eval_obj", self.value))
3135

3236
def __repr__(self):
33-
return f"{str(self[0])}={repr(self[1])}"
37+
return f"{self.__class__.__name__}({repr(self.arg)}, {repr(self.value)})"
38+
39+
def __str__(self):
40+
return f"{self.arg}={self.value}"
41+
42+
def _repr_pretty_(self, p, cycle):
43+
p.text(str(self))
3444

3545

36-
class ExpressionTuple(tuple):
37-
"""A tuple object that represents an expression.
46+
class ExpressionTuple(Sequence):
47+
"""A tuple-like object that represents an expression.
3848
39-
This object carries the underlying object, if any, and preserves it
40-
through limited forms of concatenation/cons-ing.
49+
This object caches the return value resulting from evaluation of the
50+
expression it represents. Likewise, it holds onto the "parent" expression
51+
from which it was derived (e.g. as a slice), if any, so that it can
52+
preserve the return value through limited forms of concatenation/cons-ing
53+
that would reproduce the parent expression.
4154
55+
TODO: Should probably use weakrefs for that.
4256
"""
4357

58+
__slots__ = ("_eval_obj", "_tuple", "_orig_expr")
4459
null = object()
4560

46-
def __new__(cls, *args, **kwargs):
47-
obj = super().__new__(cls, *args, **kwargs)
48-
# TODO: Consider making this a weakref.
49-
obj._eval_obj = cls.null
50-
return obj
61+
def __new__(cls, seq=None, **kwargs):
62+
63+
# XXX: This doesn't actually remove the entry from the kwargs
64+
# passed to __init__!
65+
# It does, however, remove it for the check below.
66+
kwargs.pop("eval_obj", None)
67+
68+
if seq is None and not kwargs and isinstance(seq, cls):
69+
return seq
70+
71+
res = super().__new__(cls)
72+
73+
return res
74+
75+
def __init__(self, seq=None, **kwargs):
76+
"""Create an expression tuple.
77+
78+
If the keyword 'eval_obj' is given, the `ExpressionTuple`'s
79+
evaluated object is set to the corresponding value.
80+
XXX: There is no verification/check that the arguments evaluate to the
81+
user-specified 'eval_obj', so be careful.
82+
"""
83+
84+
_eval_obj = kwargs.pop("eval_obj", self.null)
85+
etuple_kwargs = tuple(KwdPair(k, v) for k, v in kwargs.items())
86+
87+
if seq:
88+
self._tuple = tuple(seq) + etuple_kwargs
89+
else:
90+
self._tuple = etuple_kwargs
91+
92+
# TODO: Consider making these a weakrefs.
93+
self._eval_obj = _eval_obj
94+
self._orig_expr = None
5195

5296
@property
5397
def eval_obj(self):
5498
"""Return the evaluation of this expression tuple.
5599
56-
XXX: If the object isn't cached, it will be evaluated recursively.
100+
Warning: If the evaluation value isn't cached, it will be evaluated
101+
recursively.
57102
58103
"""
59-
if self._eval_obj is not ExpressionTuple.null:
104+
if self._eval_obj is not self.null:
60105
return self._eval_obj
61106
else:
62-
evaled_args = [getattr(i, "eval_obj", i) for i in self[1:]]
107+
evaled_args = [getattr(i, "eval_obj", i) for i in self._tuple[1:]]
63108
arg_grps = toolz.groupby(lambda x: isinstance(x, KwdPair), evaled_args)
64109
evaled_args = arg_grps.get(False, [])
65110
evaled_kwargs = arg_grps.get(True, [])
66111

67-
op = self[0]
112+
op = self._tuple[0]
68113
try:
69114
op_sig = inspect.signature(op)
70115
except ValueError:
71-
_eval_obj = op(*(evaled_args + [kw[1] for kw in evaled_kwargs]))
116+
# This handles some builtin function types
117+
_eval_obj = op(*(evaled_args + [kw.value for kw in evaled_kwargs]))
72118
else:
73-
op_args = op_sig.bind(*evaled_args, **dict(evaled_kwargs))
119+
op_args = op_sig.bind(*evaled_args, **{kw.arg: kw.value for kw in evaled_kwargs})
74120
op_args.apply_defaults()
75121

76122
_eval_obj = op(*op_args.args, **op_args.kwargs)
@@ -84,87 +130,104 @@ def eval_obj(self):
84130
def eval_obj(self, obj):
85131
raise ValueError("Value of evaluated expression cannot be set!")
86132

133+
def __add__(self, x):
134+
res = self._tuple + x
135+
if self._orig_expr is not None and res == self._orig_expr._tuple:
136+
return self._orig_expr
137+
return type(self)(res)
138+
139+
def __contains__(self, *args):
140+
return self._tuple.__contains__(*args)
141+
142+
def __ge__(self, *args):
143+
return self._tuple.__ge__(*args)
144+
87145
def __getitem__(self, key):
88-
# if isinstance(key, slice):
89-
# return [self.list[i] for i in xrange(key.start, key.stop, key.step)]
90-
# return self.list[key]
91-
tuple_res = super().__getitem__(key)
146+
tuple_res = self._tuple[key]
92147
if isinstance(key, slice) and isinstance(tuple_res, tuple):
93148
tuple_res = type(self)(tuple_res)
94-
tuple_res.orig_expr = self
149+
tuple_res._orig_expr = self
95150
return tuple_res
96151

97-
def __add__(self, x):
98-
res = type(self)(super().__add__(x))
99-
if res == getattr(self, "orig_expr", None):
100-
return self.orig_expr
101-
return res
152+
def __gt__(self, *args):
153+
return self._tuple.__gt__(*args)
154+
155+
def __iter__(self, *args):
156+
return self._tuple.__iter__(*args)
157+
158+
def __le__(self, *args):
159+
return self._tuple.__le__(*args)
160+
161+
def __len__(self, *args):
162+
return self._tuple.__len__(*args)
163+
164+
def __lt__(self, *args):
165+
return self._tuple.__lt__(*args)
166+
167+
def __mul__(self, *args):
168+
return self._tuple.__mul__(*args)
169+
170+
def __rmul__(self, *args):
171+
return self._tuple.__rmul__(*args)
102172

103173
def __radd__(self, x):
104-
res = type(self)(x + tuple(self))
105-
if res == getattr(self, "orig_expr", None):
106-
return self.orig_expr
107-
return res
174+
res = x + self._tuple # type(self)(x + self._tuple)
175+
if self._orig_expr is not None and res == self._orig_expr._tuple:
176+
return self._orig_expr
177+
return type(self)(res)
108178

109179
def __str__(self):
110-
return f"e({', '.join(tuple(str(i) for i in self))})"
180+
return f"e({', '.join(tuple(str(i) for i in self._tuple))})"
111181

112182
def __repr__(self):
113-
return f"ExpressionTuple({etuple_repr.repr(tuple(self))})"
183+
return f"ExpressionTuple({etuple_repr.repr(self._tuple)})"
114184

115185
def _repr_pretty_(self, p, cycle):
116186
if cycle:
117-
p.text(f"{self.__class__.__name__}(...)")
187+
p.text(f"e(...)")
118188
else:
119-
with p.group(2, f"{self.__class__.__name__}((", "))"):
120-
p.breakable()
121-
for idx, item in enumerate(self):
189+
with p.group(2, "e(", ")"):
190+
p.breakable(sep="")
191+
for idx, item in enumerate(self._tuple):
122192
if idx:
123193
p.text(",")
124194
p.breakable()
125195
p.pretty(item)
126196

197+
def __eq__(self, other):
198+
return self._tuple == other
127199

128-
def etuple(*args, **kwargs):
129-
"""Create an expression tuple from the arguments.
200+
def __hash__(self):
201+
return hash(self._tuple)
130202

131-
If the keyword 'eval_obj' is given, the `ExpressionTuple`'s
132-
evaluated object is set to the corresponding value.
133-
XXX: There is no verification/check that the arguments evaluate to the
134-
user-specified 'eval_obj', so be careful.
135203

136-
"""
137-
_eval_obj = kwargs.pop("eval_obj", ExpressionTuple.null)
138-
139-
etuple_kwargs = tuple(KwdPair(k, v) for k, v in kwargs.items())
140-
141-
res = ExpressionTuple(args + etuple_kwargs)
204+
def etuple(*args, **kwargs):
205+
"""Create an ExpressionTuple from the argument list.
142206
143-
res._eval_obj = _eval_obj
207+
In other words:
208+
etuple(1, 2, 3) == ExpressionTuple((1, 2, 3))
144209
145-
return res
210+
"""
211+
return ExpressionTuple(args, **kwargs)
146212

147213

148214
@dispatch(object)
149-
def etuplize(x, shallow=False):
215+
def etuplize(x, shallow=False, return_bad_args=False):
150216
"""Return an expression-tuple for an object (i.e. a tuple of rand and rators).
151217
152-
When evaluated, the rand and rators should [re-]construct the object. When the
153-
object cannot be given such a form, the object itself is returned.
154-
155-
NOTE: `etuplize(...)[2:]` and `arguments(...)` will *not* return
156-
the same thing by default, because the former is recursive and the latter
157-
is not. In other words, this S-expression-like "decomposition" is
158-
recursive, and, as such, it requires an inside-out evaluation to
159-
re-construct a "decomposed" object. In contrast, `operator` and
160-
`arguments` is necessarily a shallow "decomposition".
218+
When evaluated, the rand and rators should [re-]construct the object. When
219+
the object cannot be given such a form, it is simply converted to an
220+
`ExpressionTuple` and returned.
161221
162222
Parameters
163223
----------
164224
x: object
165225
Object to convert to expression-tuple form.
166226
shallow: bool
167227
Whether or not to do a shallow conversion.
228+
return_bad_args: bool
229+
Return the passed argument when its type is not appropriate, instead
230+
of raising an exception.
168231
169232
"""
170233
if isinstance(x, ExpressionTuple):
@@ -176,18 +239,23 @@ def etuplize(x, shallow=False):
176239
op = operator(x)
177240
args = arguments(x)
178241
except (IndexError, NotImplementedError):
179-
return x
242+
op = None
243+
args = x
180244

181-
assert isinstance(args, (list, tuple))
245+
if not isinstance(args, Sequence) or isinstance(args, str):
246+
if return_bad_args:
247+
return x
248+
else:
249+
raise TypeError(f"x is neither a non-str Sequence nor term: {type(x)}")
182250

183251
# Not everything in a list/tuple should be considered an expression.
184252
if not callable(op):
185-
return x
253+
return etuple(*x)
186254

187255
if shallow:
188256
et_args = args
189257
else:
190-
et_args = tuple(etuplize(a) for a in args)
258+
et_args = tuple(etuplize(a, return_bad_args=True) for a in args)
191259

192260
res = etuple(op, *et_args, eval_obj=x)
193261
return res

symbolic_pymc/relations/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222

2323
def buildo(op, args, obj):
2424
if not isvar(obj):
25-
if not (isinstance(obj, ExpressionTuple) and isinstance(args, ExpressionTuple)):
26-
obj = etuplize(obj, shallow=True)
25+
if not isvar(args):
2726
args = etuplize(args, shallow=True)
2827
oop, oargs = operator(obj), arguments(obj)
29-
return lallgreedy((eq, op, oop), (eq, args, oargs))
28+
return lallgreedy(eq(op, oop), eq(args, oargs))
3029
elif isvar(args) or isvar(op):
3130
return conso(op, args, obj)
3231
else:

symbolic_pymc/relations/graph.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,11 @@ def reduceo(relation, in_expr, out_expr):
124124

125125

126126
def graph_applyo(
127-
relation, in_graph, out_graph, preprocess_graph=partial(etuplize, shallow=True), inside=False
127+
relation,
128+
in_graph,
129+
out_graph,
130+
preprocess_graph=partial(etuplize, shallow=True, return_bad_args=True),
131+
inside=False,
128132
):
129133
"""Relate the fixed-points of two term-graphs under a given relation.
130134

0 commit comments

Comments
 (0)