Skip to content

Commit 4c7cc60

Browse files
committed
curry: by default, TypeError if args remaining when exiting top-level curry context; enh: accept just tuple as the pythonic multiple-return-values thing (not liśt); greppability: always use ordering (list, tuple) in cases where both are ok
1 parent 199fe41 commit 4c7cc60

File tree

6 files changed

+104
-51
lines changed

6 files changed

+104
-51
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,9 @@ p | runpipe
223223
assert fibos == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
224224
```
225225

226-
Both one-in-one-out (*1-to-1*) and n-in-m-out (*n-to-m*) pipes are provided. The 1-to-1 versions have names suffixed with ``1``. The use case is one-argument functions that return one value (which may also be a tuple or list).
226+
Both one-in-one-out (*1-to-1*) and n-in-m-out (*n-to-m*) pipes are provided. The 1-to-1 versions have names suffixed with ``1``. The use case is one-argument functions that return one value (which may also be a tuple).
227227

228-
In the n-to-m versions, when a function returns a tuple or list, it is unpacked to the argument list of the next function in the pipe. At ``getvalue`` or ``runpipe`` time, the tuple wrapper (if any) around the final result is discarded if it contains only one item. (This allows the n-to-m versions to work also with a single value, as long as it is not a tuple or list.) The main use case is computations that deal with multiple values, the number of which may also change during the computation (as long as there are as many "slots" on both sides of each individual connection).
228+
In the n-to-m versions, when a function returns a tuple, it is unpacked to the argument list of the next function in the pipe. At ``getvalue`` or ``runpipe`` time, the tuple wrapper (if any) around the final result is discarded if it contains only one item. (This allows the n-to-m versions to work also with a single value, as long as it is not a tuple.) The main use case is computations that deal with multiple values, the number of which may also change during the computation (as long as there are as many "slots" on both sides of each individual connection).
229229

230230

231231
### Introduce local bindings: ``let``, ``letrec``
@@ -1096,9 +1096,9 @@ Some overlap with [toolz](https://github.com/pytoolz/toolz) and [funcy](https://
10961096
- As a regular function, `curry` itself is curried à la Racket. If it gets extra arguments (beside the function ``f``), they are the first step. This helps eliminate many parentheses.
10971097
- **Caution**: If the positional arities of ``f`` cannot be inspected, currying fails, raising ``UnknownArity``. This may happen with builtins such as ``operator.add``.
10981098
- `composel`, `composer`: both left-to-right and right-to-left function composition, to help readability.
1099-
- Any number of positional arguments is supported, with the same rules as in the pipe system. Multiple return values packed into a tuple or list are unpacked to the argument list of the next function in the chain.
1099+
- Any number of positional arguments is supported, with the same rules as in the pipe system. Multiple return values packed into a tuple are unpacked to the argument list of the next function in the chain.
11001100
- `composelc`, `composerc`: curry each function before composing them. Useful with passthrough.
1101-
- `composel1`, `composer1`: 1-in-1-out chains (faster; also useful for a single value that is a tuple or list).
1101+
- `composel1`, `composer1`: 1-in-1-out chains (faster; also useful for a single value that is a tuple).
11021102
- suffix `i` to use with an iterable (`composeli`, `composeri`, `composelci`, `composerci`, `composel1i`, `composer1i`)
11031103
- `andf`, `orf`, `notf`: compose predicates (like Racket's `conjoin`, `disjoin`, `negate`).
11041104
- `rotate`: a cousin of `flip`, for permuting positional arguments.
@@ -1450,7 +1450,7 @@ assert tuple(uniq((1, 1, 2, 2, 2, 1, 2, 2, 4, 3, 4, 3, 3))) == (1, 2, 1, 2, 4, 3
14501450
assert tuple(flatten1(((1, 2), (3, (4, 5), 6), (7, 8, 9)))) == (1, 2, 3, (4, 5), 6, 7, 8, 9)
14511451
assert tuple(flatten(((1, 2), (3, (4, 5), 6), (7, 8, 9)))) == (1, 2, 3, 4, 5, 6, 7, 8, 9)
14521452
1453-
is_nested = lambda sublist: all(isinstance(x, (tuple, list)) for x in sublist)
1453+
is_nested = lambda sublist: all(isinstance(x, (list, tuple)) for x in sublist)
14541454
assert tuple(flatten((((1, 2), (3, 4)), (5, 6)), is_nested)) == ((1, 2), (3, 4), (5, 6))
14551455
14561456
data = (((1, 2), ((3, 4), (5, 6)), 7), ((8, 9), (10, 11)))

unpythonic/ec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def s(loop, acc=0, i=0):
152152
assert f() == 15
153153
"""
154154
if tags is not None:
155-
if isinstance(tags, (tuple, list)): # multiple tags
155+
if isinstance(tags, tuple): # multiple tags
156156
tags = set(tags)
157157
else: # single tag
158158
tags = set((tags,))

unpythonic/fold.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,19 @@ def mymap_one2(f, iterable):
393393
assert doubler(ll(1, 2, 3)) == ll(2, 4, 6)
394394

395395
# curry supports passing through on the right any args over the max arity.
396-
assert curry(double, 2, "foo") == (4, "foo") # arity of double is 1
397-
398-
# In passthrough, if an intermediate result is a callable,
399-
# it is invoked on the remaining positional args:
396+
# If an intermediate result is a callable, it is invoked on the remaining
397+
# positional args:
400398
assert curry(mymap_one4, double, ll(1, 2, 3)) == ll(2, 4, 6)
401399

400+
# But having any args remaining when the top-level curry context exits
401+
# is an error:
402+
try:
403+
curry(double, 2, "foo")
404+
except TypeError:
405+
pass
406+
else:
407+
assert False # top-level curry context exited with args remaining
408+
402409
# This also works; curried f takes one argument and the second one is passed
403410
# through on the right; this two-tuple then ends up as the arguments to cons.
404411
mymap_one5 = lambda f: curry(foldr, composer(cons, curry(f)), nil)

unpythonic/fun.py

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from unpythonic.arity import arities
2323
from unpythonic.fold import reducel
24+
from unpythonic.dynscope import dyn, make_dynvar
2425

2526
def memoize(f):
2627
"""Decorator: memoize the function f.
@@ -61,6 +62,8 @@ def memoized(*args, **kwargs):
6162
# return memo[k]
6263
# return memoized
6364

65+
make_dynvar(_curry_context=None)
66+
make_dynvar(curry_toplevel_passthrough=False)
6467
def curry(f, *args, **kwargs):
6568
"""Decorator: curry the function f.
6669
@@ -100,13 +103,9 @@ def foo(a, b, *, c, d):
100103
101104
**Passthrough**:
102105
103-
If too many args are given, any extra ones are passed through on the right::
104-
105-
double = lambda x: 2 * x
106-
assert curry(double)(2, "foo") == (4, "foo")
107-
108-
In passthrough, if an intermediate result is callable it is invoked
109-
on the remaining positional args::
106+
If too many args are given, any extra ones are passed through on the right.
107+
If an intermediate result is callable, it is invoked on the remaining
108+
positional args::
110109
111110
map_one = lambda f: (curry(foldr))(composer(cons, to1st(f)), nil)
112111
assert curry(map_one)(double, ll(1, 2, 3)) == ll(2, 4, 6)
@@ -118,6 +117,19 @@ def foo(a, b, *, c, d):
118117
For simplicity, in passthrough, all kwargs are consumed in the first step
119118
for which too many positional args were supplied.
120119
120+
By default, if any passed-through positional args are still remaining when
121+
the currently top-level curry context exits, ``curry`` raises ``TypeError``,
122+
because such usage often indicates a bug.
123+
124+
This behavior can be locally modified by setting the dynvar
125+
``curry_toplevel_passthrough``::
126+
127+
with dyn.let(curry_toplevel_passthrough=True):
128+
curry(double, 2, "foo") == (4, "foo")
129+
130+
Because it is a dynvar, it affects all ``curry`` calls in its dynamic extent,
131+
including ones inside library functions such as ``composerc`` or ``pipec``.
132+
121133
**Curry itself is curried**:
122134
123135
When invoked as a regular function (not decorator), curry itself is curried.
@@ -128,8 +140,6 @@ def foo(a, b, *, c, d):
128140
129141
This comboes with passthrough::
130142
131-
assert curry(double, 2, "foo") == (4, "foo")
132-
133143
mymap = lambda f: curry(foldr, composerc(cons, f), nil)
134144
add = lambda x, y: x + y
135145
assert curry(mymap, add, ll(1, 2, 3), ll(4, 5, 6)) == ll(5, 7, 9)
@@ -151,22 +161,28 @@ def foo(a, b, *, c, d):
151161
min_arity, max_arity = arities(f)
152162
@wraps(f)
153163
def curried(*args, **kwargs):
154-
if len(args) < min_arity:
155-
return curry(partial(f, *args, **kwargs))
156-
# passthrough on right, like https://github.com/Technologicat/spicy
157-
if len(args) > max_arity:
158-
now_args, later_args = args[:max_arity], args[max_arity:]
159-
now_result = f(*now_args, **kwargs) # use up all kwargs now
160-
if callable(now_result):
161-
# curry it now, to sustain the chain in case we have
162-
# too many (or too few) args for it.
163-
if not iscurried(now_result):
164-
now_result = curry(now_result)
165-
return now_result(*later_args)
166-
if isinstance(now_result, (tuple, list)):
167-
return tuple(now_result) + later_args
168-
return (now_result,) + later_args
169-
return f(*args, **kwargs)
164+
outerctx = dyn._curry_context
165+
with dyn.let(_curry_context=(outerctx, f)):
166+
if len(args) < min_arity:
167+
return curry(partial(f, *args, **kwargs))
168+
# passthrough on right, like https://github.com/Technologicat/spicy
169+
if len(args) > max_arity:
170+
now_args, later_args = args[:max_arity], args[max_arity:]
171+
now_result = f(*now_args, **kwargs) # use up all kwargs now
172+
if callable(now_result):
173+
# curry it now, to sustain the chain in case we have
174+
# too many (or too few) args for it.
175+
if not iscurried(now_result):
176+
now_result = curry(now_result)
177+
return now_result(*later_args)
178+
if not (outerctx or dyn.curry_toplevel_passthrough):
179+
raise TypeError("Top-level curry context exited with {:d} arg(s) remaining: {}".format(len(later_args), later_args))
180+
# pass through to the curried procedure waiting in outerctx
181+
# (e.g. in a curried compose chain)
182+
if isinstance(now_result, tuple):
183+
return now_result + later_args
184+
return (now_result,) + later_args
185+
return f(*args, **kwargs)
170186
curried._is_curried_function = True # stash for detection
171187
# curry itself is curried: if we get args, they're the first step
172188
if args or kwargs:
@@ -387,10 +403,18 @@ def composel1i(iterable):
387403
def _make_compose(direction): # "left", "right"
388404
def compose_two(f, g):
389405
def composed(*args):
390-
a = g(*args)
391-
# we could duck-test but this is more predictable for the user
392-
# (consider chaining functions that manipulate a generator).
393-
if isinstance(a, (list, tuple)):
406+
# co-operate with curry: provide a top-level curry context
407+
# to allow passthrough from the function that is applied first
408+
# to the function that is applied second.
409+
if iscurried(f):
410+
with dyn.let(_curry_context=(dyn._curry_context, composed)):
411+
a = g(*args)
412+
else:
413+
a = g(*args)
414+
# we could duck-test, but this is more predictable for the user
415+
# (consider chaining functions that manipulate a generator), and
416+
# tuple specifically is the pythonic multiple-return-values thing.
417+
if isinstance(a, tuple):
394418
return f(*a)
395419
return f(a)
396420
return composed
@@ -408,7 +432,7 @@ def composer(*fs):
408432
409433
This mirrors the standard mathematical convention (f ∘ g)(x) ≡ f(g(x)).
410434
411-
At each step, if the output from a function is a list or a tuple,
435+
At each step, if the output from a function is a tuple,
412436
it is unpacked to the argument list of the next function. Otherwise,
413437
we assume the output is intended to be fed to the next function as-is.
414438

unpythonic/it.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ def flatten(iterable, pred=None):
449449
450450
E.g. to flatten only those items that contain only tuples::
451451
452-
is_nested = lambda e: all(isinstance(x, (tuple, list)) for x in e)
452+
is_nested = lambda e: all(isinstance(x, (list, tuple)) for x in e)
453453
data = (((1, 2), (3, 4)), (5, 6))
454454
assert tuple(flatten(data, is_nested)) == ((1, 2), (3, 4), (5, 6))
455455
"""
@@ -479,7 +479,7 @@ def flatten_in(iterable, pred=None):
479479
480480
Example::
481481
482-
is_nested = lambda e: all(isinstance(x, (tuple, list)) for x in e)
482+
is_nested = lambda e: all(isinstance(x, (list, tuple)) for x in e)
483483
data = (((1, 2), ((3, 4), (5, 6)), 7), ((8, 9), (10, 11)))
484484
assert tuple(flatten(data, is_nested)) == \\
485485
(((1, 2), ((3, 4), (5, 6)), 7), (8, 9), (10, 11))
@@ -636,7 +636,7 @@ def sum_and_diff(a, b):
636636
assert tuple(flatten(((1, 2), (3, (4, 5), 6), (7, 8, 9)))) == (1, 2, 3, 4, 5, 6, 7, 8, 9)
637637
assert tuple(flatten1(((1, 2), (3, (4, 5), 6), (7, 8, 9)))) == (1, 2, 3, (4, 5), 6, 7, 8, 9)
638638

639-
is_nested = lambda e: all(isinstance(x, (tuple, list)) for x in e)
639+
is_nested = lambda e: all(isinstance(x, (list, tuple)) for x in e)
640640
assert tuple(flatten((((1, 2), (3, 4)), (5, 6)), is_nested)) == ((1, 2), (3, 4), (5, 6))
641641

642642
data = (((1, 2), ((3, 4), (5, 6)), 7), ((8, 9), (10, 11)))

unpythonic/seq.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from collections import namedtuple
1212
from unpythonic.env import env
1313
from unpythonic.misc import call
14-
from unpythonic.fun import curry
14+
from unpythonic.fun import curry, iscurried
15+
from unpythonic.dynscope import dyn
1516
from unpythonic.arity import arity_includes, UnknownArity
1617

1718
# sequence side effects in a lambda
@@ -246,7 +247,7 @@ def pipe(values0, *bodys):
246247
The only restriction is that each function must take as many positional
247248
arguments as the previous one returns.
248249
249-
At each step, if the output from a function is a list or a tuple,
250+
At each step, if the output from a function is a tuple,
250251
it is unpacked to the argument list of the next function. Otherwise,
251252
we assume the output is intended to be fed to the next function as-is.
252253
@@ -271,12 +272,24 @@ def pipe(values0, *bodys):
271272
assert (a, b) == (13, "got foo")
272273
"""
273274
xs = values0
274-
for update in bodys:
275-
if isinstance(xs, (list, tuple)):
275+
n = len(bodys)
276+
def doit():
277+
nonlocal xs
278+
if isinstance(xs, tuple):
276279
xs = update(*xs)
277280
else:
278281
xs = update(xs)
279-
if isinstance(xs, (list, tuple)):
282+
for k, update in enumerate(bodys):
283+
islast = (k == n - 1)
284+
# co-operate with curry: provide a top-level curry context
285+
# to allow passthrough from a pipelined function to the next
286+
# (except the last one, since it exits the curry context).
287+
if iscurried(update) and not islast:
288+
with dyn.let(_curry_context=(dyn._curry_context, update)):
289+
doit()
290+
else:
291+
doit()
292+
if isinstance(xs, tuple):
280293
return xs if len(xs) > 1 else xs[0]
281294
return xs
282295

@@ -311,11 +324,11 @@ def __or__(self, f):
311324
if f is getvalue:
312325
return xs if len(xs) > 1 else xs[0]
313326
cls = self.__class__
314-
if isinstance(xs, (list, tuple)):
327+
if isinstance(xs, tuple):
315328
newxs = f(*xs)
316329
else:
317330
newxs = f(xs)
318-
if isinstance(newxs, (list, tuple)):
331+
if isinstance(newxs, tuple):
319332
return cls(*newxs)
320333
return cls(newxs)
321334
def __repr__(self):
@@ -356,7 +369,7 @@ def __or__(self, f):
356369
if f is runpipe: # compute now
357370
vs = self._xs
358371
for g in self._funcs:
359-
if isinstance(vs, (list, tuple)):
372+
if isinstance(vs, tuple):
360373
vs = g(*vs)
361374
else:
362375
vs = g(vs)
@@ -545,6 +558,15 @@ def test():
545558
lambda x, y: (x * 2, y + 1))
546559
assert (a, b) == (4, 3)
547560

561+
try:
562+
a, b = pipec((1, 2),
563+
lambda x: x + 1,
564+
lambda x: x * 2)
565+
except TypeError:
566+
pass
567+
else:
568+
assert False # error if the curry context exits with args remaining
569+
548570
# optional shell-like syntax
549571
assert piped1(42) | double | inc | getvalue == 85
550572

0 commit comments

Comments
 (0)