Skip to content

Commit cef82ca

Browse files
committed
add @generic support to curry and arity utilities
`curry` and the utilities `arities`, `required_kwargs`, and `optional_kwargs` now support `@generic` functions.
1 parent 9eaa4c7 commit cef82ca

File tree

6 files changed

+238
-152
lines changed

6 files changed

+238
-152
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ The same applies if you need the macro parts of `unpythonic` (i.e. import anythi
8787
- Add function `isgeneric` to detect whether a callable has been declared `@generic`.
8888
- Add function `methods`: display a list of multimethods of a generic function.
8989
- It is now possible to dispatch on a homogeneous type of contents collected by a `**kwargs` parameter.
90+
- `curry` now supports `@generic` functions.
91+
- The utilities `arities`, `required_kwargs`, and `optional_kwargs` now support `@generic` functions.
9092
- Add `unpythonic.excutil.reraise_in` (expr form), `unpythonic.excutil.reraise` (block form): conveniently remap library exception types to application exception types. Idea from [Alexis King (2016): Four months with Haskell](https://lexi-lambda.github.io/blog/2016/06/12/four-months-with-haskell/).
9193
- Add variants of the above for the conditions-and-restarts system: `unpythonic.conditions.resignal_in`, `unpythonic.conditions.resignal`. The new signal is sent using the same error-handling protocol as the original signal, so that e.g. an `error` remains an `error` even if re-signaling changes its type.
9294
- All documentation files now have a quick navigation section to skip to another part of the docs. (For all except the README, it's at the top.)

doc/features.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,6 +3015,10 @@ The core idea can be expressed in fewer than 100 lines of Python; ours is (as of
30153015
30163016
*Docstrings of the multimethods are now automatically concatenated to make up the docstring of the generic function, so you can document each multimethod separately.*
30173017
3018+
*`curry` now supports `@generic`. In the case where the **number** of positional arguments supplied so far matches at least one multimethod, but there is no match for the given combination of argument **types**, `curry` waits for more arguments (returning the curried function).*
3019+
3020+
**CAUTION**: *Determining whether there **could** be a match for a `@generic` is the only type checking performed by ``curry``. When using ``curry`` with ``@generic`` or ``@typed``, argument type errors are only detected when the actual call triggers - just like in code using ``curry`` and traditional run-time ``isinstance`` checks. This may make it hard to debug.*
3021+
30183022
*It is now possible to dispatch also on a homogeneous type of contents collected by a `**kwargs` parameter. In the type signature, use `typing.Dict[str, mytype]`. Note that in this use, the key type is always `str`.*
30193023
30203024
The ``generic`` decorator allows creating multiple-dispatch generic functions with type annotation syntax. We also provide some friendly utilities: ``typed`` creates a single-method generic with the same syntax (i.e. provides a compact notation for writing dynamic type checking code), and ``isoftype`` (which powers the first two) is the big sister of ``isinstance``, with support for many (but unfortunately not all) features of the ``typing`` standard library module.
@@ -3036,8 +3040,6 @@ If several multimethods of the same generic function match the arguments given,
30363040
30373041
**CAUTION**: The winning multimethod is chosen differently from Julia, where the most specific multimethod wins. Doing that requires a more careful type analysis than what we have here.
30383042
3039-
**CAUTION**: `@generic` does not currently work with `curry`. Adding support requires changes to the already complex logic in `curry`; it is not high on the priority list.
3040-
30413043
The details are best explained by example:
30423044
30433045
```python
@@ -3165,8 +3167,6 @@ jack(3.14) # TypeError
31653167
31663168
For which features of the ``typing`` stdlib module are supported, see ``isoftype`` below.
31673169
3168-
**CAUTION**: When using ``typed`` with ``curry``, the type checking (and hence ``TypeError``, if any) only occurs when the actual call triggers. Code using that combination may be hard to debug.
3169-
31703170
31713171
#### ``isoftype``: the big sister of ``isinstance``
31723172

unpythonic/arity.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ def arities(f):
217217
does not implicitly provide a `self`, because there is none to be had. This
218218
behavior is reflected in the return value of `arities`.)
219219
220+
If `f` is `@generic` (see `unpythonic.dispatch`), we scan its multimethods,
221+
and return the smallest `min_arity` and the largest `max_arity`.
222+
220223
Parameters:
221224
`f`: function
222225
The function to inspect.
@@ -238,6 +241,20 @@ def arities(f):
238241
return _builtin_arities[f]
239242
except TypeError: # f is of an unhashable type
240243
pass
244+
245+
# Integration with the multiple-dispatch system (multimethods).
246+
from .dispatch import isgeneric, list_methods # circular import
247+
if isgeneric(f):
248+
min_lower = _infty
249+
max_upper = 0
250+
for (thecallable, type_signature) in list_methods(f):
251+
lower, upper = arities(thecallable) # let UnknownArity propagate
252+
if lower < min_lower:
253+
min_lower = lower
254+
if upper > max_upper:
255+
max_upper = upper
256+
return min_lower, max_upper
257+
241258
try:
242259
lower = 0
243260
upper = 0
@@ -258,25 +275,40 @@ def arities(f):
258275
raise UnknownArity(*e.args)
259276

260277
def required_kwargs(f):
261-
"""Return a set containing the names of required name-only arguments of f.
278+
"""Return a set containing the names of required name-only arguments of `f`.
262279
263-
"Required": has no default.
280+
*Required* means the parameter has no default.
264281
265-
Raises UnknownArity if inspection failed.
282+
If `f` is `@generic` (see `unpythonic.dispatch`), we scan its multimethods,
283+
and return the names of required kwargs accepted by *any* of its multimethods.
284+
285+
Raises `UnknownArity` if inspection failed.
266286
"""
267287
return _kwargs(f, optionals=False)
268288

269289
def optional_kwargs(f):
270-
"""Return a set containing the names of optional name-only arguments of f.
290+
"""Return a set containing the names of optional name-only arguments of `f`.
271291
272-
"Optional": has a default.
292+
*Optional* means the parameter has a default.
273293
274-
Raises UnknownArity if inspection failed.
294+
If `f` is `@generic` (see `unpythonic.dispatch`), we scan its multimethods,
295+
and return the names of optional kwargs accepted by *any* of its multimethods.
296+
297+
Raises `UnknownArity` if inspection failed.
275298
"""
276299
return _kwargs(f, optionals=True)
277300

278301
def _kwargs(f, optionals=True):
279302
f, _ = getfunc(f)
303+
304+
# Integration with the multiple-dispatch system (multimethods).
305+
from .dispatch import isgeneric, list_methods # circular import
306+
if isgeneric(f):
307+
thekwargs = {}
308+
for (thecallable, type_signature) in list_methods(f):
309+
thekwargs.update(_kwargs(thecallable, optionals=optionals))
310+
return thekwargs
311+
280312
try:
281313
if optionals:
282314
pred = lambda v: v.default is not Parameter.empty # optionals

0 commit comments

Comments
 (0)