effectful 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- effectful/__init__.py +0 -0
- effectful/handlers/__init__.py +0 -0
- effectful/handlers/indexed.py +320 -0
- effectful/handlers/numbers.py +259 -0
- effectful/handlers/pyro.py +466 -0
- effectful/handlers/torch.py +572 -0
- effectful/internals/__init__.py +0 -0
- effectful/internals/base_impl.py +259 -0
- effectful/internals/runtime.py +78 -0
- effectful/ops/__init__.py +0 -0
- effectful/ops/semantics.py +329 -0
- effectful/ops/syntax.py +523 -0
- effectful/ops/types.py +110 -0
- effectful/py.typed +0 -0
- effectful-0.0.1.dist-info/LICENSE.md +202 -0
- effectful-0.0.1.dist-info/METADATA +170 -0
- effectful-0.0.1.dist-info/RECORD +19 -0
- effectful-0.0.1.dist-info/WHEEL +5 -0
- effectful-0.0.1.dist-info/top_level.txt +1 -0
effectful/ops/syntax.py
ADDED
@@ -0,0 +1,523 @@
|
|
1
|
+
import collections
|
2
|
+
import dataclasses
|
3
|
+
import functools
|
4
|
+
import typing
|
5
|
+
from typing import (
|
6
|
+
Annotated,
|
7
|
+
Callable,
|
8
|
+
Generic,
|
9
|
+
Mapping,
|
10
|
+
Optional,
|
11
|
+
Sequence,
|
12
|
+
Type,
|
13
|
+
TypeVar,
|
14
|
+
)
|
15
|
+
|
16
|
+
import tree
|
17
|
+
from typing_extensions import Concatenate, ParamSpec
|
18
|
+
|
19
|
+
from effectful.ops.types import ArgAnnotation, Expr, Interpretation, Operation, Term
|
20
|
+
|
21
|
+
P = ParamSpec("P")
|
22
|
+
Q = ParamSpec("Q")
|
23
|
+
S = TypeVar("S")
|
24
|
+
T = TypeVar("T")
|
25
|
+
V = TypeVar("V")
|
26
|
+
|
27
|
+
|
28
|
+
@dataclasses.dataclass
|
29
|
+
class Bound(ArgAnnotation):
|
30
|
+
scope: int = 0
|
31
|
+
|
32
|
+
|
33
|
+
@dataclasses.dataclass
|
34
|
+
class Scoped(ArgAnnotation):
|
35
|
+
scope: int = 0
|
36
|
+
|
37
|
+
|
38
|
+
class NoDefaultRule(Exception):
|
39
|
+
"""Raised in an operation's signature to indicate that the operation has no default rule."""
|
40
|
+
|
41
|
+
pass
|
42
|
+
|
43
|
+
|
44
|
+
@typing.overload
|
45
|
+
def defop(t: Type[T], *, name: Optional[str] = None) -> Operation[[], T]: ...
|
46
|
+
|
47
|
+
|
48
|
+
@typing.overload
|
49
|
+
def defop(t: Callable[P, T], *, name: Optional[str] = None) -> Operation[P, T]: ...
|
50
|
+
|
51
|
+
|
52
|
+
@typing.overload
|
53
|
+
def defop(t: Operation[P, T], *, name: Optional[str] = None) -> Operation[P, T]: ...
|
54
|
+
|
55
|
+
|
56
|
+
def defop(t, *, name=None):
|
57
|
+
"""Creates a fresh :class:`Operation`.
|
58
|
+
|
59
|
+
:param t: May be a type, callable, or :class:`Operation`. If a type, the
|
60
|
+
operation will have no arguments and return the type. If a callable,
|
61
|
+
the operation will have the same signature as the callable, but with
|
62
|
+
no default rule. If an operation, the operation will be a distinct
|
63
|
+
copy of the operation.
|
64
|
+
:param name: Optional name for the operation.
|
65
|
+
:returns: A fresh operation.
|
66
|
+
|
67
|
+
.. note::
|
68
|
+
|
69
|
+
The result of :func:`defop` is always fresh (i.e. ``defop(f) != defop(f)``).
|
70
|
+
|
71
|
+
**Example usage**:
|
72
|
+
|
73
|
+
* Defining an operation:
|
74
|
+
|
75
|
+
This example defines an operation that selects one of two integers:
|
76
|
+
|
77
|
+
>>> @defop
|
78
|
+
... def select(x: int, y: int) -> int:
|
79
|
+
... return x
|
80
|
+
|
81
|
+
The operation can be called like a regular function. By default, ``select``
|
82
|
+
returns the first argument:
|
83
|
+
|
84
|
+
>>> select(1, 2)
|
85
|
+
1
|
86
|
+
|
87
|
+
We can change its behavior by installing a ``select`` handler:
|
88
|
+
|
89
|
+
>>> from effectful.ops.semantics import handler
|
90
|
+
>>> with handler({select: lambda x, y: y}):
|
91
|
+
... print(select(1, 2))
|
92
|
+
2
|
93
|
+
|
94
|
+
* Defining an operation with no default rule:
|
95
|
+
|
96
|
+
We can use :func:`defop` and the
|
97
|
+
:exc:`effectful.internals.sugar.NoDefaultRule` exception to define an
|
98
|
+
operation with no default rule:
|
99
|
+
|
100
|
+
>>> @defop
|
101
|
+
... def add(x: int, y: int) -> int:
|
102
|
+
... raise NoDefaultRule
|
103
|
+
>>> add(1, 2)
|
104
|
+
add(1, 2)
|
105
|
+
|
106
|
+
When an operation has no default rule, the free rule is used instead, which
|
107
|
+
constructs a term of the operation applied to its arguments. This feature
|
108
|
+
can be used to conveniently define the syntax of a domain-specific language.
|
109
|
+
|
110
|
+
* Defining free variables:
|
111
|
+
|
112
|
+
Passing :func:`defop` a type is a handy way to create a free variable.
|
113
|
+
|
114
|
+
>>> import effectful.handlers.operator
|
115
|
+
>>> from effectful.ops.semantics import evaluate
|
116
|
+
>>> x = defop(int, name='x')
|
117
|
+
>>> y = x() + 1
|
118
|
+
|
119
|
+
``y`` is free in ``x``, so it is not fully evaluated:
|
120
|
+
|
121
|
+
>>> y
|
122
|
+
add(x(), 1)
|
123
|
+
|
124
|
+
We bind ``x`` by installing a handler for it:
|
125
|
+
|
126
|
+
>>> with handler({x: lambda: 2}):
|
127
|
+
... print(evaluate(y))
|
128
|
+
3
|
129
|
+
|
130
|
+
.. note::
|
131
|
+
|
132
|
+
Because the result of :func:`defop` is always fresh, it's important to
|
133
|
+
be careful with variable identity.
|
134
|
+
|
135
|
+
Two variables with the same name are not equal:
|
136
|
+
|
137
|
+
>>> x1 = defop(int, name='x')
|
138
|
+
>>> x2 = defop(int, name='x')
|
139
|
+
>>> x1 == x2
|
140
|
+
False
|
141
|
+
|
142
|
+
This means that to correctly bind a variable, you must use the same
|
143
|
+
operation object. In this example, ``scale`` returns a term with a free
|
144
|
+
variable ``x``:
|
145
|
+
|
146
|
+
>>> import effectful.handlers.operator
|
147
|
+
>>> def scale(a: float) -> float:
|
148
|
+
... x = defop(float, name='x')
|
149
|
+
... return x() * a
|
150
|
+
|
151
|
+
Binding the variable ``x`` by creating a fresh operation object does not
|
152
|
+
|
153
|
+
>>> term = scale(3.0)
|
154
|
+
>>> x = defop(float, name='x')
|
155
|
+
>>> with handler({x: lambda: 2.0}):
|
156
|
+
... print(evaluate(term))
|
157
|
+
mul(x(), 3.0)
|
158
|
+
|
159
|
+
This does:
|
160
|
+
|
161
|
+
>>> from effectful.ops.semantics import fvsof
|
162
|
+
>>> correct_x = [v for v in fvsof(term) if str(x) == 'x'][0]
|
163
|
+
>>> with handler({correct_x: lambda: 2.0}):
|
164
|
+
... print(evaluate(term))
|
165
|
+
6.0
|
166
|
+
|
167
|
+
* Defining a fresh :class:`Operation`:
|
168
|
+
|
169
|
+
Passing :func:`defop` an :class:`Operation` creates a fresh operation with
|
170
|
+
the same name and signature, but no default rule.
|
171
|
+
|
172
|
+
>>> fresh_select = defop(select)
|
173
|
+
>>> fresh_select(1, 2)
|
174
|
+
select(1, 2)
|
175
|
+
|
176
|
+
The new operation is distinct from the original:
|
177
|
+
|
178
|
+
>>> with handler({select: lambda x, y: y}):
|
179
|
+
... print(select(1, 2), fresh_select(1, 2))
|
180
|
+
2 select(1, 2)
|
181
|
+
|
182
|
+
>>> with handler({fresh_select: lambda x, y: y}):
|
183
|
+
... print(select(1, 2), fresh_select(1, 2))
|
184
|
+
1 2
|
185
|
+
|
186
|
+
"""
|
187
|
+
|
188
|
+
if isinstance(t, Operation):
|
189
|
+
|
190
|
+
def func(*args, **kwargs):
|
191
|
+
raise NoDefaultRule
|
192
|
+
|
193
|
+
functools.update_wrapper(func, t)
|
194
|
+
return defop(func, name=name)
|
195
|
+
elif isinstance(t, type):
|
196
|
+
|
197
|
+
def func() -> t: # type: ignore
|
198
|
+
raise NoDefaultRule
|
199
|
+
|
200
|
+
func.__name__ = name or t.__name__
|
201
|
+
return typing.cast(Operation[[], T], defop(func, name=name))
|
202
|
+
elif isinstance(t, collections.abc.Callable):
|
203
|
+
from effectful.internals.base_impl import _BaseOperation
|
204
|
+
|
205
|
+
op = _BaseOperation(t)
|
206
|
+
op.__name__ = name or t.__name__
|
207
|
+
return op
|
208
|
+
else:
|
209
|
+
raise ValueError(f"expected type or callable, got {t}")
|
210
|
+
|
211
|
+
|
212
|
+
@defop
|
213
|
+
def deffn(
|
214
|
+
body: T,
|
215
|
+
*args: Annotated[Operation, Bound()],
|
216
|
+
**kwargs: Annotated[Operation, Bound()],
|
217
|
+
) -> Callable[..., T]:
|
218
|
+
"""An operation that represents a lambda function.
|
219
|
+
|
220
|
+
:param body: The body of the function.
|
221
|
+
:type body: T
|
222
|
+
:param args: Operations representing the positional arguments of the function.
|
223
|
+
:type args: Annotated[Operation, Bound()]
|
224
|
+
:param kwargs: Operations representing the keyword arguments of the function.
|
225
|
+
:type kwargs: Annotated[Operation, Bound()]
|
226
|
+
:returns: A callable term.
|
227
|
+
:rtype: Callable[..., T]
|
228
|
+
|
229
|
+
:func:`deffn` terms are eliminated by the :func:`call` operation, which
|
230
|
+
performs beta-reduction.
|
231
|
+
|
232
|
+
**Example usage**:
|
233
|
+
|
234
|
+
Here :func:`deffn` is used to define a term that represents the function
|
235
|
+
``lambda x, y=1: 2 * x + y``:
|
236
|
+
|
237
|
+
>>> import effectful.handlers.operator
|
238
|
+
>>> x, y = defop(int, name='x'), defop(int, name='y')
|
239
|
+
>>> term = deffn(2 * x() + y(), x, y=y)
|
240
|
+
>>> term
|
241
|
+
deffn(add(mul(2, x()), y()), x, y=y)
|
242
|
+
>>> term(3, y=4)
|
243
|
+
10
|
244
|
+
|
245
|
+
.. note::
|
246
|
+
|
247
|
+
In general, avoid using :func:`deffn` directly. Instead, use
|
248
|
+
:func:`defterm` to convert a function to a term because it will
|
249
|
+
automatically create the right free variables.
|
250
|
+
|
251
|
+
"""
|
252
|
+
raise NoDefaultRule
|
253
|
+
|
254
|
+
|
255
|
+
class _CustomSingleDispatchCallable(Generic[P, T]):
|
256
|
+
def __init__(
|
257
|
+
self, func: Callable[Concatenate[Callable[[type], Callable[P, T]], P], T]
|
258
|
+
):
|
259
|
+
self._func = func
|
260
|
+
self._registry = functools.singledispatch(func)
|
261
|
+
functools.update_wrapper(self, func)
|
262
|
+
|
263
|
+
@property
|
264
|
+
def dispatch(self):
|
265
|
+
return self._registry.dispatch
|
266
|
+
|
267
|
+
@property
|
268
|
+
def register(self):
|
269
|
+
return self._registry.register
|
270
|
+
|
271
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
272
|
+
return self._func(self.dispatch, *args, **kwargs)
|
273
|
+
|
274
|
+
|
275
|
+
@_CustomSingleDispatchCallable
|
276
|
+
def defterm(dispatch, value: T) -> Expr[T]:
|
277
|
+
"""Convert a value to a term, using the type of the value to dispatch.
|
278
|
+
|
279
|
+
:param value: The value to convert.
|
280
|
+
:type value: T
|
281
|
+
:returns: A term.
|
282
|
+
:rtype: Expr[T]
|
283
|
+
|
284
|
+
**Example usage**:
|
285
|
+
|
286
|
+
:func:`defterm` can be passed a function, and it will convert that function
|
287
|
+
to a term by calling it with appropriately typed free variables:
|
288
|
+
|
289
|
+
>>> def incr(x: int) -> int:
|
290
|
+
... return x + 1
|
291
|
+
>>> term = defterm(incr)
|
292
|
+
>>> term
|
293
|
+
deffn(add(int(), 1), int)
|
294
|
+
>>> term(2)
|
295
|
+
3
|
296
|
+
|
297
|
+
"""
|
298
|
+
if isinstance(value, Term):
|
299
|
+
return value
|
300
|
+
else:
|
301
|
+
return dispatch(type(value))(value)
|
302
|
+
|
303
|
+
|
304
|
+
@_CustomSingleDispatchCallable
|
305
|
+
def defdata(dispatch, expr: Term[T]) -> Expr[T]:
|
306
|
+
"""Converts a term so that it is an instance of its inferred type.
|
307
|
+
|
308
|
+
:param expr: The term to convert.
|
309
|
+
:type expr: Term[T]
|
310
|
+
:returns: An instance of ``T``.
|
311
|
+
:rtype: Expr[T]
|
312
|
+
|
313
|
+
This function is called by :func:`__free_rule__`, so conversions
|
314
|
+
resgistered with :func:`defdata` are automatically applied when terms are
|
315
|
+
constructed.
|
316
|
+
|
317
|
+
.. note::
|
318
|
+
|
319
|
+
This function is not likely to be called by users of the effectful
|
320
|
+
library, but they may wish to register implementations for additional
|
321
|
+
types.
|
322
|
+
|
323
|
+
**Example usage**:
|
324
|
+
|
325
|
+
This is how callable terms are implemented:
|
326
|
+
|
327
|
+
.. code-block:: python
|
328
|
+
|
329
|
+
class _CallableTerm(Generic[P, T], _BaseTerm[collections.abc.Callable[P, T]]):
|
330
|
+
def __call__(self, *args: Expr, **kwargs: Expr) -> Expr[T]:
|
331
|
+
from effectful.ops.semantics import call
|
332
|
+
|
333
|
+
return call(self, *args, **kwargs)
|
334
|
+
|
335
|
+
@defdata.register(collections.abc.Callable)
|
336
|
+
def _(op, args, kwargs):
|
337
|
+
return _CallableTerm(op, args, kwargs)
|
338
|
+
|
339
|
+
When a :class:`Callable` term is passed to :func:`defdata`, it is
|
340
|
+
reconstructed as a :class:`_CallableTerm`, which implements the
|
341
|
+
:func:`__call__` method.
|
342
|
+
|
343
|
+
"""
|
344
|
+
from effectful.ops.semantics import typeof
|
345
|
+
|
346
|
+
if isinstance(expr, Term):
|
347
|
+
impl: Callable[[Operation[..., T], Sequence, Mapping[str, object]], Expr[T]]
|
348
|
+
impl = dispatch(typeof(expr)) # type: ignore
|
349
|
+
return impl(expr.op, expr.args, expr.kwargs)
|
350
|
+
else:
|
351
|
+
return expr
|
352
|
+
|
353
|
+
|
354
|
+
@defterm.register(object)
|
355
|
+
@defterm.register(Operation)
|
356
|
+
@defterm.register(Term)
|
357
|
+
def _(value: T) -> T:
|
358
|
+
return value
|
359
|
+
|
360
|
+
|
361
|
+
@defdata.register(object)
|
362
|
+
def _(op, args, kwargs):
|
363
|
+
from effectful.internals.base_impl import _BaseTerm
|
364
|
+
|
365
|
+
return _BaseTerm(op, args, kwargs)
|
366
|
+
|
367
|
+
|
368
|
+
@defdata.register(collections.abc.Callable)
|
369
|
+
def _(op, args, kwargs):
|
370
|
+
from effectful.internals.base_impl import _CallableTerm
|
371
|
+
|
372
|
+
return _CallableTerm(op, args, kwargs)
|
373
|
+
|
374
|
+
|
375
|
+
@defterm.register(collections.abc.Callable)
|
376
|
+
def _(fn: Callable[P, T]):
|
377
|
+
from effectful.internals.base_impl import _unembed_callable
|
378
|
+
|
379
|
+
return _unembed_callable(fn)
|
380
|
+
|
381
|
+
|
382
|
+
def syntactic_eq(x: Expr[T], other: Expr[T]) -> bool:
|
383
|
+
"""Syntactic equality, ignoring the interpretation of the terms.
|
384
|
+
|
385
|
+
:param x: A term.
|
386
|
+
:type x: Expr[T]
|
387
|
+
:param other: Another term.
|
388
|
+
:type other: Expr[T]
|
389
|
+
:returns: ``True`` if the terms are syntactically equal and ``False`` otherwise.
|
390
|
+
"""
|
391
|
+
if isinstance(x, Term) and isinstance(other, Term):
|
392
|
+
op, args, kwargs = x.op, x.args, x.kwargs
|
393
|
+
op2, args2, kwargs2 = other.op, other.args, other.kwargs
|
394
|
+
try:
|
395
|
+
tree.assert_same_structure(
|
396
|
+
(op, args, kwargs), (op2, args2, kwargs2), check_types=True
|
397
|
+
)
|
398
|
+
except (TypeError, ValueError):
|
399
|
+
return False
|
400
|
+
return all(
|
401
|
+
tree.flatten(
|
402
|
+
tree.map_structure(
|
403
|
+
syntactic_eq, (op, args, kwargs), (op2, args2, kwargs2)
|
404
|
+
)
|
405
|
+
)
|
406
|
+
)
|
407
|
+
elif isinstance(x, Term) or isinstance(other, Term):
|
408
|
+
return False
|
409
|
+
else:
|
410
|
+
return x == other
|
411
|
+
|
412
|
+
|
413
|
+
class ObjectInterpretation(Generic[T, V], Interpretation[T, V]):
|
414
|
+
"""A helper superclass for defining an ``Interpretation`` of many
|
415
|
+
:class:`~effectful.ops.types.Operation` instances with shared state or behavior.
|
416
|
+
|
417
|
+
You can mark specific methods in the definition of an
|
418
|
+
:class:`ObjectInterpretation` with operations using the :func:`implements`
|
419
|
+
decorator. The :class:`ObjectInterpretation` object itself is an
|
420
|
+
``Interpretation`` (mapping from :class:`~effectful.ops.types.Operation` to :class:`~typing.Callable`)
|
421
|
+
|
422
|
+
>>> from effectful.ops.semantics import handler
|
423
|
+
>>> @defop
|
424
|
+
... def read_box():
|
425
|
+
... pass
|
426
|
+
...
|
427
|
+
>>> @defop
|
428
|
+
... def write_box(new_value):
|
429
|
+
... pass
|
430
|
+
...
|
431
|
+
>>> class StatefulBox(ObjectInterpretation):
|
432
|
+
... def __init__(self, init=None):
|
433
|
+
... super().__init__()
|
434
|
+
... self.stored = init
|
435
|
+
... @implements(read_box)
|
436
|
+
... def whatever(self):
|
437
|
+
... return self.stored
|
438
|
+
... @implements(write_box)
|
439
|
+
... def write_box(self, new_value):
|
440
|
+
... self.stored = new_value
|
441
|
+
...
|
442
|
+
>>> first_box = StatefulBox(init="First Starting Value")
|
443
|
+
>>> second_box = StatefulBox(init="Second Starting Value")
|
444
|
+
>>> with handler(first_box):
|
445
|
+
... print(read_box())
|
446
|
+
... write_box("New Value")
|
447
|
+
... print(read_box())
|
448
|
+
...
|
449
|
+
First Starting Value
|
450
|
+
New Value
|
451
|
+
>>> with handler(second_box):
|
452
|
+
... print(read_box())
|
453
|
+
Second Starting Value
|
454
|
+
>>> with handler(first_box):
|
455
|
+
... print(read_box())
|
456
|
+
New Value
|
457
|
+
|
458
|
+
"""
|
459
|
+
|
460
|
+
# This is a weird hack to get around the fact that
|
461
|
+
# the default meta-class runs __set_name__ before __init__subclass__.
|
462
|
+
# We basically store the implementations here temporarily
|
463
|
+
# until __init__subclass__ is called.
|
464
|
+
# This dict is shared by all `Implementation`s,
|
465
|
+
# so we need to clear it when we're done.
|
466
|
+
_temporary_implementations: dict[Operation[..., T], Callable[..., V]] = dict()
|
467
|
+
implementations: dict[Operation[..., T], Callable[..., V]] = dict()
|
468
|
+
|
469
|
+
@classmethod
|
470
|
+
def __init_subclass__(cls, **kwargs):
|
471
|
+
super().__init_subclass__(**kwargs)
|
472
|
+
cls.implementations = ObjectInterpretation._temporary_implementations.copy()
|
473
|
+
|
474
|
+
for sup in cls.mro():
|
475
|
+
if issubclass(sup, ObjectInterpretation):
|
476
|
+
cls.implementations = {**sup.implementations, **cls.implementations}
|
477
|
+
|
478
|
+
ObjectInterpretation._temporary_implementations.clear()
|
479
|
+
|
480
|
+
def __iter__(self):
|
481
|
+
return iter(self.implementations)
|
482
|
+
|
483
|
+
def __len__(self):
|
484
|
+
return len(self.implementations)
|
485
|
+
|
486
|
+
def __getitem__(self, item: Operation[..., T]) -> Callable[..., V]:
|
487
|
+
return self.implementations[item].__get__(self, type(self))
|
488
|
+
|
489
|
+
|
490
|
+
class _ImplementedOperation(Generic[P, Q, T, V]):
|
491
|
+
impl: Optional[Callable[Q, V]]
|
492
|
+
op: Operation[P, T]
|
493
|
+
|
494
|
+
def __init__(self, op: Operation[P, T]):
|
495
|
+
self.op = op
|
496
|
+
self.impl = None
|
497
|
+
|
498
|
+
def __get__(
|
499
|
+
self, instance: ObjectInterpretation[T, V], owner: type
|
500
|
+
) -> Callable[..., V]:
|
501
|
+
assert self.impl is not None
|
502
|
+
|
503
|
+
return self.impl.__get__(instance, owner)
|
504
|
+
|
505
|
+
def __call__(self, impl: Callable[Q, V]):
|
506
|
+
self.impl = impl
|
507
|
+
return self
|
508
|
+
|
509
|
+
def __set_name__(self, owner: ObjectInterpretation[T, V], name):
|
510
|
+
assert self.impl is not None
|
511
|
+
assert self.op is not None
|
512
|
+
owner._temporary_implementations[self.op] = self.impl
|
513
|
+
|
514
|
+
|
515
|
+
def implements(op: Operation[P, V]):
|
516
|
+
"""Marks a method in an :class:`ObjectInterpretation` as the implementation of a
|
517
|
+
particular abstract :class:`Operation`.
|
518
|
+
|
519
|
+
When passed an :class:`Operation`, returns a method decorator which installs
|
520
|
+
the given method as the implementation of the given :class:`Operation`.
|
521
|
+
|
522
|
+
"""
|
523
|
+
return _ImplementedOperation(op)
|
effectful/ops/types.py
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import abc
|
4
|
+
import typing
|
5
|
+
from typing import Any, Callable, Generic, Mapping, Sequence, Set, Type, TypeVar, Union
|
6
|
+
|
7
|
+
from typing_extensions import ParamSpec
|
8
|
+
|
9
|
+
P = ParamSpec("P")
|
10
|
+
Q = ParamSpec("Q")
|
11
|
+
S = TypeVar("S")
|
12
|
+
T = TypeVar("T")
|
13
|
+
V = TypeVar("V")
|
14
|
+
|
15
|
+
|
16
|
+
class Operation(abc.ABC, Generic[Q, V]):
|
17
|
+
"""An abstract class representing an effect that can be implemented by an effect handler.
|
18
|
+
|
19
|
+
.. note::
|
20
|
+
|
21
|
+
Do not use :class:`Operation` directly. Instead, use :func:`defop` to define operations.
|
22
|
+
|
23
|
+
"""
|
24
|
+
|
25
|
+
@abc.abstractmethod
|
26
|
+
def __eq__(self, other):
|
27
|
+
raise NotImplementedError
|
28
|
+
|
29
|
+
@abc.abstractmethod
|
30
|
+
def __hash__(self):
|
31
|
+
raise NotImplementedError
|
32
|
+
|
33
|
+
@abc.abstractmethod
|
34
|
+
def __default_rule__(self, *args: Q.args, **kwargs: Q.kwargs) -> "Expr[V]":
|
35
|
+
"""The default rule is used when the operation is not handled.
|
36
|
+
|
37
|
+
If no default rule is supplied, the free rule is used instead.
|
38
|
+
"""
|
39
|
+
raise NotImplementedError
|
40
|
+
|
41
|
+
@abc.abstractmethod
|
42
|
+
def __free_rule__(self, *args: Q.args, **kwargs: Q.kwargs) -> "Expr[V]":
|
43
|
+
"""Returns a term for the operation applied to arguments."""
|
44
|
+
raise NotImplementedError
|
45
|
+
|
46
|
+
@abc.abstractmethod
|
47
|
+
def __type_rule__(self, *args: Q.args, **kwargs: Q.kwargs) -> Type[V]:
|
48
|
+
"""Returns the type of the operation applied to arguments."""
|
49
|
+
raise NotImplementedError
|
50
|
+
|
51
|
+
@abc.abstractmethod
|
52
|
+
def __fvs_rule__(self, *args: Q.args, **kwargs: Q.kwargs) -> Set["Operation"]:
|
53
|
+
"""Returns the free variables of the operation applied to arguments."""
|
54
|
+
raise NotImplementedError
|
55
|
+
|
56
|
+
@abc.abstractmethod
|
57
|
+
def __repr_rule__(self, *args: Q.args, **kwargs: Q.kwargs) -> str:
|
58
|
+
raise NotImplementedError
|
59
|
+
|
60
|
+
@typing.final
|
61
|
+
def __call__(self, *args: Q.args, **kwargs: Q.kwargs) -> V:
|
62
|
+
from effectful.internals.runtime import get_interpretation
|
63
|
+
from effectful.ops.semantics import apply
|
64
|
+
|
65
|
+
return apply.__default_rule__(get_interpretation(), self, *args, **kwargs) # type: ignore
|
66
|
+
|
67
|
+
|
68
|
+
class Term(abc.ABC, Generic[T]):
|
69
|
+
"""A term in an effectful computation is a is a tree of :class:`Operation`
|
70
|
+
applied to values.
|
71
|
+
|
72
|
+
"""
|
73
|
+
|
74
|
+
__match_args__ = ("op", "args", "kwargs")
|
75
|
+
|
76
|
+
@property
|
77
|
+
@abc.abstractmethod
|
78
|
+
def op(self) -> Operation[..., T]:
|
79
|
+
"""Abstract property for the operation."""
|
80
|
+
pass
|
81
|
+
|
82
|
+
@property
|
83
|
+
@abc.abstractmethod
|
84
|
+
def args(self) -> Sequence["Expr[Any]"]:
|
85
|
+
"""Abstract property for the arguments."""
|
86
|
+
pass
|
87
|
+
|
88
|
+
@property
|
89
|
+
@abc.abstractmethod
|
90
|
+
def kwargs(self) -> Mapping[str, "Expr[Any]"]:
|
91
|
+
"""Abstract property for the keyword arguments."""
|
92
|
+
pass
|
93
|
+
|
94
|
+
def __repr__(self) -> str:
|
95
|
+
from effectful.internals.runtime import interpreter
|
96
|
+
from effectful.ops.semantics import apply, evaluate
|
97
|
+
|
98
|
+
with interpreter({apply: lambda _, op, *a, **k: op.__repr_rule__(*a, **k)}):
|
99
|
+
return evaluate(self) # type: ignore
|
100
|
+
|
101
|
+
|
102
|
+
#: An expression is either a value or a term.
|
103
|
+
Expr = Union[T, Term[T]]
|
104
|
+
|
105
|
+
#: An interpretation is a mapping from operations to their implementations.
|
106
|
+
Interpretation = Mapping[Operation[..., T], Callable[..., V]]
|
107
|
+
|
108
|
+
|
109
|
+
class ArgAnnotation:
|
110
|
+
pass
|
effectful/py.typed
ADDED
File without changes
|