Skip to content

Commit 1e2a8d2

Browse files
committed
small improvements to pipe system
1 parent ac0633f commit 1e2a8d2

2 files changed

Lines changed: 93 additions & 59 deletions

File tree

README.md

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Other design considerations are simplicity, robustness, and minimal dependencies
1010
- [Multi-expression lambdas](#multi-expression-lambdas)
1111
- [Sequence side effects: ``begin``](#sequence-side-effects-begin)
1212
- [Stuff imperative code into a lambda: ``do``](#stuff-imperative-code-into-a-lambda-do)
13-
- [Sequence one-input one-output functions: ``pipe``, ``piped``, ``lazy_piped``](#sequence-one-input-one-output-functions-pipe-piped-lazy_piped)
13+
- [Sequence functions: ``pipe``, ``piped``, ``lazy_piped``](#sequence-functions-pipe-piped-lazy_piped)
1414
- [Introduce local bindings: ``let``, ``letrec``](#introduce-local-bindings-let-letrec)
1515
- [The environment: ``env``](#the-environment-env) (details)
1616
- [Tail call optimization (TCO) / explicit continuations](#tail-call-optimization-tco--explicit-continuations)
@@ -134,9 +134,9 @@ do(lambda e: print("hello 2 from 'do'"), # delayed because lambda e: ...
134134

135135
Unlike ``begin`` (and ``begin0``), there is no separate ``lazy_do`` (``lazy_do0``), because using a ``lambda e: ...`` wrapper will already delay evaluation of an item. If you want a lazy variant, just wrap each item (also those which don't otherwise need it).
136136

137-
#### Sequence one-input one-output functions: ``pipe``, ``piped``, ``lazy_piped``
137+
#### Sequence functions: ``pipe``, ``piped``, ``lazy_piped``
138138

139-
Similar to Racket's [threading macros](https://docs.racket-lang.org/threading/). A pipe performs a sequence of operations, starting from an initial value, and then returns the final value:
139+
Similar to Racket's [threading macros](https://docs.racket-lang.org/threading/). A pipe performs a sequence of operations, starting from an initial value, and then returns the final value. It's just function composition, but with an emphasis on data flow, which helps improve readability:
140140

141141
```python
142142
from unpythonic import pipe
@@ -148,53 +148,56 @@ x = pipe(42, double, inc)
148148
assert x == 85
149149
```
150150

151-
This removes the need to read the source code backwards (compare `x = inc(double(42))`), while also making `x` have only a single definition at the call site.
152-
153151
Optional **shell-like syntax**, with purely functional updates:
154152

155153
```python
156-
from unpythonic import piped, get
154+
from unpythonic import piped, getvalue
157155

158-
x = piped(42) | double | inc | get
156+
x = piped(42) | double | inc | getvalue
159157
assert x == 85
160158

161159
p = piped(42) | double
162-
assert p | inc | get == 85
163-
assert p | get == 84 # p itself is never modified by the pipe system
160+
assert p | inc | getvalue == 85
161+
assert p | getvalue == 84 # p itself is never modified by the pipe system
164162
```
165163

166-
Set up a pipe by calling ``piped`` for the initial value. Pipe into the sentinel ``get`` to exit the pipe and return the current value.
164+
Set up a pipe by calling ``piped`` for the initial value. Pipe into the sentinel ``getvalue`` to exit the pipe and return the current value.
167165

168-
**Lazy pipes** for mutable initial values. Computation runs at ``get`` time:
166+
**Lazy pipes**, useful for mutable initial values:
169167

170168
```python
171-
from unpythonic import lazy_piped, get
169+
from unpythonic import lazy_piped1, runpipe
172170

173171
lst = [1]
174172
def append_succ(l):
175173
l.append(l[-1] + 1)
176-
return l # important, handed to the next function in the pipe
177-
p = lazy_piped(lst) | append_succ | append_succ # plan a computation
174+
return l # this return value is handed to the next function in the pipe
175+
p = lazy_piped1(lst) | append_succ | append_succ # plan a computation
178176
assert lst == [1] # nothing done yet
179-
p | get # run the computation
177+
p | runpipe # run the computation
180178
assert lst == [1, 2, 3] # now the side effect has updated lst.
181179
```
182180

183181
Lazy pipe as an unfold:
184182

185183
```python
184+
from unpythonic import lazy_piped, runpipe
185+
186186
fibos = []
187-
def nextfibo(state):
188-
a, b = state
187+
def nextfibo(a, b): # multiple arguments allowed
189188
fibos.append(a) # store result by side effect
190189
return (b, a + b) # new state, handed to next function in the pipe
191-
p = lazy_piped((1, 1)) # load initial state into a lazy pipe
190+
p = lazy_piped(1, 1) # load initial state
192191
for _ in range(10): # set up pipeline
193192
p = p | nextfibo
194-
p | get # run it
195-
print(fibos)
193+
p | runpipe
194+
assert fibos == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
196195
```
197196

197+
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).
198+
199+
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.)
200+
198201

199202
### Introduce local bindings: ``let``, ``letrec``
200203

unpythonic/seq.py

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
__all__ = ["begin", "begin0", "lazy_begin", "lazy_begin0",
66
"pipe1", "piped1", "lazy_piped1",
7-
"pipe", "piped", "lazy_piped", "get",
7+
"pipe", "piped", "getvalue", "lazy_piped", "runpipe",
88
"do", "do0", "assign"]
99

1010
from collections import namedtuple
@@ -138,10 +138,11 @@ def pipe1(value0, *bodys):
138138
return x
139139

140140
@call # make a singleton
141-
class get: # sentinel with a nice repr
141+
class getvalue: # sentinel with a nice repr
142142
"""Sentinel; pipe into this to exit a shell-like pipe and return the current value."""
143143
def __repr__(self):
144144
return "<sentinel for pipe exit>"
145+
runpipe = getvalue # same thing, but semantically better name for lazy pipes
145146

146147
class piped1:
147148
"""Shell-like piping syntax.
@@ -156,21 +157,21 @@ def __or__(self, f):
156157
157158
Return a ``piped`` object, for chainability.
158159
159-
As the only exception, if ``f`` is the sentinel ``get``, return the
160-
current value (useful for exiting the pipe).
160+
As the only exception, if ``f`` is the sentinel ``getvalue``,
161+
return the current value (useful for exiting the pipe).
161162
162163
A new ``piped`` object is created at each step of piping; the "update"
163164
is purely functional, nothing is overwritten.
164165
165166
Examples::
166167
167-
x = piped1(42) | double | inc | get
168+
x = piped1(42) | double | inc | getvalue
168169
169170
y = piped1(42) | double
170-
assert y | inc | get == 85
171-
assert y | get == 84 # y is not modified
171+
assert y | inc | getvalue == 85
172+
assert y | getvalue == 84 # y is not modified
172173
"""
173-
if f is get:
174+
if f is getvalue:
174175
return self._x
175176
else:
176177
cls = self.__class__
@@ -179,7 +180,7 @@ def __repr__(self):
179180
return "<piped1 at 0x{:x}; value {}>".format(id(self), self._x)
180181

181182
class lazy_piped1:
182-
"""Like piped, but apply the functions later, at get time.
183+
"""Like piped, but apply the functions later.
183184
184185
This matters if the initial value is mutable:
185186
@@ -188,7 +189,7 @@ class lazy_piped1:
188189
the pipeline.
189190
190191
- ``lazy_piped`` just sets up a computation, and performs it when eventually
191-
piped into ``get``. The computation always looks up the latest state
192+
piped into ``runpipe``. The computation always looks up the latest state
192193
of the initial value.
193194
194195
Another way to say this is that ``lazy_piped`` looks up the initial value
@@ -204,17 +205,17 @@ def __init__(self, x, *, _funcs=None):
204205
def __or__(self, f):
205206
"""Pipe the value into f; but just plan to do so, don't perform it yet.
206207
207-
To run the stored computation, pipe into ``get``.
208+
To run the stored computation, pipe into ``runpipe``.
208209
209210
Examples::
210211
211212
lst = [1]
212213
def append_succ(l):
213214
l.append(l[-1] + 1)
214215
return l # important, handed to the next function in the pipe
215-
p = lazy_piped(lst) | append_succ | append_succ # plan a computation
216+
p = lazy_piped1(lst) | append_succ | append_succ # plan a computation
216217
assert lst == [1] # nothing done yet
217-
p | get # run the computation
218+
p | runpipe # run the computation
218219
assert lst == [1, 2, 3] # now the side effect has updated lst.
219220
220221
# lazy pipe as an unfold
@@ -223,13 +224,13 @@ def nextfibo(state):
223224
a, b = state
224225
fibos.append(a) # store result by side effect
225226
return (b, a + b) # new state, handed to next function in the pipe
226-
p = lazy_piped((1, 1)) # load initial state into a lazy pipe
227+
p = lazy_piped1((1, 1)) # load initial state into a lazy pipe
227228
for _ in range(10): # set up pipeline
228229
p = p | nextfibo
229-
p | get # run it
230+
p | runpipe
230231
print(fibos)
231232
"""
232-
if f is get: # compute now
233+
if f is runpipe: # compute now
233234
v = self._x
234235
for g in self._funcs:
235236
v = g(v)
@@ -291,30 +292,46 @@ def __or__(self, f):
291292
292293
f = lambda x, y: (2*x, y+1)
293294
g = lambda x, y: (x+1, 2*y)
294-
x = piped(2, 3) | f | g | get # --> (5, 8)
295+
x = piped(2, 3) | f | g | getvalue # --> (5, 8)
295296
"""
296-
if f is get:
297-
return self._xs
297+
xs = self._xs
298+
if f is getvalue:
299+
return xs if len(xs) > 1 else xs[0]
298300
else:
299301
cls = self.__class__
300-
if isinstance(self._xs, (list, tuple)):
301-
return cls(*f(*self._xs))
302+
if isinstance(xs, (list, tuple)):
303+
newxs = f(*xs)
302304
else:
303-
return cls(f(self._xs))
305+
newxs = f(xs)
306+
if isinstance(newxs, (list, tuple)):
307+
return cls(*newxs)
308+
else:
309+
return cls(newxs)
304310
def __repr__(self):
305311
return "<piped at 0x{:x}; values {}>".format(id(self), self._xs)
306312

307313
class lazy_piped:
308314
"""Like lazy_piped1, but for any number of inputs/outputs at each step.
309315
310-
Example::
316+
Examples::
311317
312318
p1 = lazy_piped(2, 3)
313319
p2 = p1 | (lambda x, y: (x + 1, 2 * y, "foo"))
314320
p3 = p2 | (lambda x, y, s: (x * 2, y + 1, "got {}".format(s)))
315321
p4 = p3 | (lambda x, y, s: (x + y, s))
316322
# nothing done yet!
317-
assert (p4 | get) == (13, "got foo")
323+
assert (p4 | runpipe) == (13, "got foo")
324+
325+
# lazy pipe as an unfold
326+
fibos = []
327+
def nextfibo(a, b): # now two arguments
328+
fibos.append(a)
329+
return (b, a + b) # two return values, still expressed as a tuple
330+
p = lazy_piped(1, 1)
331+
for _ in range(10):
332+
p = p | nextfibo
333+
p | runpipe
334+
print(fibos)
318335
"""
319336
def __init__(self, *xs, _funcs=None):
320337
"""Set up a lazy pipe and load the initial values xs into it.
@@ -325,14 +342,14 @@ def __init__(self, *xs, _funcs=None):
325342
self._funcs = _funcs or ()
326343
def __or__(self, f):
327344
"""Pipe the values into f; but just plan to do so, don't perform it yet."""
328-
if f is get: # compute now
345+
if f is runpipe: # compute now
329346
vs = self._xs
330347
for g in self._funcs:
331348
if isinstance(vs, (list, tuple)):
332349
vs = g(*vs)
333350
else:
334351
vs = g(vs)
335-
return vs
352+
return vs if len(vs) > 1 else vs[0]
336353
else:
337354
# just pass on the references to the original xs.
338355
cls = self.__class__
@@ -492,20 +509,20 @@ def test():
492509
assert (a, b) == (13, "got foo")
493510

494511
# with optional shell-like syntax
495-
assert piped1(42) | double | inc | get == 85
512+
assert piped1(42) | double | inc | getvalue == 85
496513

497514
y = piped1(42) | double
498-
assert y | inc | get == 85
499-
assert y | get == 84 # y is never modified by the pipe system
515+
assert y | inc | getvalue == 85
516+
assert y | getvalue == 84 # y is never modified by the pipe system
500517

501-
# lazy pipe: compute at get time
518+
# lazy pipe: compute later
502519
lst = [1]
503520
def append_succ(l):
504521
l.append(l[-1] + 1)
505522
return l # important, handed to the next function in the pipe
506523
p = lazy_piped1(lst) | append_succ | append_succ # plan a computation
507524
assert lst == [1] # nothing done yet
508-
p | get # run the computation
525+
p | runpipe # run the computation
509526
assert lst == [1, 2, 3] # now the side effect has updated lst.
510527

511528
# lazy pipe as an unfold
@@ -514,27 +531,41 @@ def nextfibo(state):
514531
a, b = state
515532
fibos.append(a) # store result by side effect
516533
return (b, a + b) # new state, handed to next function in the pipe
517-
p = lazy_piped1((1, 1)) # load initial state into a lazy pipe
534+
p = lazy_piped1((1, 1)) # load initial state into a lazy pipe
518535
for _ in range(10): # set up pipeline
519536
p = p | nextfibo
520-
p | get # run it
521-
print(fibos)
537+
p | runpipe
538+
assert fibos == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
522539

523540
# multi-arg version
524541
f = lambda x, y: (2*x, y+1)
525542
g = lambda x, y: (x+1, 2*y)
526-
x = piped(2, 3) | f | g | get # --> (5, 8)
543+
x = piped(2, 3) | f | g | getvalue # --> (5, 8)
527544
assert x == (5, 8)
528545

546+
# abuse multi-arg version for single-arg case
547+
assert piped(42) | double | inc | getvalue == 85
548+
529549
p1 = lazy_piped(2, 3)
530550
p2 = p1 | (lambda x, y: (x + 1, 2 * y, "foo"))
531551
p3 = p2 | (lambda x, y, s: (x * 2, y + 1, "got {}".format(s)))
532552
p4 = p3 | (lambda x, y, s: (x + y, s))
533553
# nothing done yet, and all computations purely functional:
534-
assert (p1 | get) == (2, 3)
535-
assert (p2 | get) == (3, 6, "foo") # runs the chain up to p2
536-
assert (p3 | get) == (6, 7, "got foo") # runs the chain up to p3
537-
assert (p4 | get) == (13, "got foo")
554+
assert (p1 | runpipe) == (2, 3)
555+
assert (p2 | runpipe) == (3, 6, "foo") # runs the chain up to p2
556+
assert (p3 | runpipe) == (6, 7, "got foo") # runs the chain up to p3
557+
assert (p4 | runpipe) == (13, "got foo")
558+
559+
# lazy pipe as an unfold
560+
fibos = []
561+
def nextfibo(a, b): # now two arguments
562+
fibos.append(a)
563+
return (b, a + b) # two return values, still expressed as a tuple
564+
p = lazy_piped(1, 1)
565+
for _ in range(10):
566+
p = p | nextfibo
567+
p | runpipe
568+
assert fibos == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
538569

539570
# do: improved begin() that can name intermediate results
540571
y = do(assign(x=17),

0 commit comments

Comments
 (0)