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.
@@ -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