Skip to content

Commit 8115ad7

Browse files
committed
Revise multiple-return-values, add named return values.
Also, improve lazify support for function composition utilities such as the `pipe` family.
1 parent aa547e2 commit 8115ad7

19 files changed

Lines changed: 666 additions & 233 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ The same applies if you need the macro parts of `unpythonic` (i.e. import anythi
145145
- Drop support for deprecated argument format for `raisef`. Now the usage is `raisef(exc)` or `raisef(exc, cause=...)`. These correspond exactly to `raise exc` and `raise exc from ...`, respectively.
146146

147147
- **Other backward-incompatible API changes.**
148+
- Multiple-return-value handling changed. Resolves issue [#32](https://github.com/Technologicat/unpythonic/issues/32).
149+
- Multiple return values are now denoted as `Values`, available from the top-level namespace of `unpythonic`.
150+
- The `Values` constructor accepts both positional and named arguments. Passing in named arguments creates **named return values**. This completes the symmetry between argument passing and returns.
151+
- Most of the time, it's still fine to return a tuple and destructure that; but in contexts where it is important to distinguish between a single `tuple` return value and multiple return values, it is preferable to use `Values`.
152+
- In any utilities that deal with function composition, if your intent is multiple-return-values, **it is now mandatory to return a `Values`** instead of a `tuple`:
153+
- `curry`
154+
- `pipe` family
155+
- `compose` family
156+
- All multiple-return-values in code using the `with continuations` macro. (The continuations system essentially composes continuation functions.)
148157
- The lazy evaluation tools `lazy`, `Lazy`, and the quick lambda `f` (underscore notation for Python) are now provided by `unpythonic` as `unpythonic.syntax.lazy`, `unpythonic.lazyutil.Lazy`, and `unpythonic.syntax.f`, because they used to be provided by `macropy`, and `mcpyrate` does not provide them.
149158
- **API differences.**
150159
- The macros `lazy` and `f` can be imported from the syntax interface module, `unpythonic.syntax`, and the class `Lazy` is available at the top level of `unpythonic`.

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
- For example:
6363
- Not only a summarizing `minmax` utility, but `running_minmax` as well. The former is then just a one-liner expressed in terms of the latter.
6464
- `foldl` accepts multiple iterables, has a switch to terminate either on the shortest or on the longest input, and takes its arguments in a curry-friendly order. It also *requires* at least one iterable, so that `curry` knows to not trigger the call until at least one iterable has been provided.
65-
- `curry` changes Python's reduction semantics to be more similar to Haskell's, to pass extra arguments through on the right, and keep calling if an intermediate result is a function, and there are still such passed-through arguments remaining. This extends what can be expressed concisely, [for example](http://www.cse.chalmers.se/~rjmh/Papers/whyfp.html) a classic lispy `map` is `curry(lambda f: curry(foldr, composerc(cons, f), nil))`. Feed that a function and an iterable, and get a linked list with the mapped results. Note the arity mismatch; `f` is 1-to-1, but `cons` is 2-to-1.
65+
- `curry` changes Python's reduction semantics to be more similar to Haskell's, to pass extra arguments through, and keep calling if an intermediate result is a function, and there are still such passed-through arguments remaining. This extends what can be expressed concisely, [for example](http://www.cse.chalmers.se/~rjmh/Papers/whyfp.html) a classic lispy `map` is `curry(lambda f: curry(foldr, composerc(cons, f), nil))`. Feed that a function and an iterable, and get a linked list with the mapped results. Note the arity mismatch; `f` is 1-to-1, but `cons` is 2-to-1.
66+
- `curry` also supports our `@generic` functions, and named return values...
6667
- **Make features work together** when it makes sense. Aim at composability. Try to make features orthogonal when reasonably possible, so that making them work together requires no extra effort. When not possible, purposefully minimizing friction in interaction between features makes for a coherent, easily understandable language extension.
6768

6869
- **Be concise but readable**, like in mathematics.

doc/dialects/lispython.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ We also import some macros and functions to serve as dialect builtins:
8585
- All ``let[]`` and ``do[]`` constructs from ``unpythonic.syntax``
8686
- ``cons``, ``car``, ``cdr``, ``ll``, ``llist``, ``nil``, ``prod``
8787
- ``dyn``, for dynamic assignment
88+
- ``Values``, for returning multiple values and/or named return values. (This ties in to `unpythonic`'s function composition subsystem, e.g. `curry`, the `pipe` family, the `compose` family, and the `with continuations` macro.)
8889

8990
For detailed documentation of the language features, see [``unpythonic.syntax``](https://github.com/Technologicat/unpythonic/tree/master/doc/macros.md), especially the macros ``tco``, ``autoreturn``, ``multilambda``, ``namedlambda``, ``quicklambda``, ``let`` and ``do``.
9091

doc/features.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -998,15 +998,17 @@ Things missing from the standard library.
998998
- **Changed in v0.15.0.** `curry` supports both positional and named arguments, and binds arguments to function parameters like Python itself does. The call triggers when all parameters are bound, regardless of whether they were passed by position or by name, and at which step of the currying process they were passed.
999999
- **Changed in v0.15.0.** `unpythonic`'s multiple-dispatch system (`@generic`, `@typed`) is supported. `curry` looks for an exact match first, then a match with extra args/kwargs, and finally a partial match. If there is still no match, this implies that at least one parameter would get a binding that fails the type check. In such a case `TypeError` regarding failed multiple dispatch is raised.
10001000
- **Changed in v0.15.0.** If the function being curried is `@generic` or `@typed`, or has type annotations on its parameters, the parameters being passed in are type-checked. A type mismatch immediately raises `TypeError`. This helps support [fail-fast](https://en.wikipedia.org/wiki/Fail-fast) in code using `curry`.
1001-
- Passthrough when too many args (à la Haskell; or [spicy](https://github.com/Technologicat/spicy) for Racket). Positional args are passed through **on the right**.
1002-
- If the intermediate result of a passthrough is callable, it is (curried and) invoked on the remaining args and kwargs. This helps with some instances of [point-free style](https://en.wikipedia.org/wiki/Tacit_programming).
1003-
- If more positional args are still remaining when the top-level curry context exits, by default ``TypeError`` is raised.
1001+
- Passthrough for args/kwargs that are incompatible with the target function's call signature (à la Haskell; or [spicy](https://github.com/Technologicat/spicy) for Racket).
1002+
- Here *incompatible* means too many positional args, or named args that have no corresponding parameter. (Note that if the function has a `**kwargs` parameter, then all named args are considered compatible, because it absorbs anything.)
1003+
- Multiple return values (both positional and named) are denoted using `Values` (which see). A standard return value is considered to consist of one positional return value only.
1004+
- Positional args are passed through **on the right**. Any positional return values of the curried function are prepended, on the left.
1005+
- If the first positional return value of an intermediate result of a passthrough is callable, it is (curried and) invoked on the remaining args and kwargs, after merging the rest of the return values into the args and kwargs. This helps with some instances of [point-free style](https://en.wikipedia.org/wiki/Tacit_programming).
1006+
- If more args/kwargs are still remaining when the top-level curry context exits, by default ``TypeError`` is raised.
10041007
- To override, set the dynvar ``curry_context``. It is a list representing the stack of currently active curry contexts. A context is any object, a human-readable label is fine. See below for an example.
10051008
- To set the dynvar, `from unpythonic import dyn`, and then `with dyn.let(curry_context=...):`.
1006-
- Even with the upgrades in v0.15.0, passing through *named* args to an outer curry context is not supported. This may or may not change in the future; fixing this requires support for named return values. See issue [#32](https://github.com/Technologicat/unpythonic/issues/32).
10071009
- Can be used both as a decorator and as a regular function.
10081010
- 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.
1009-
- **Caution**: If the signature of ``f`` cannot be inspected, currying fails, raising ``ValueError``, like ``inspect.signature`` does. This may happen with builtins such as ``list.append``, ``operator.add``, ``print`` or ``range``.
1011+
- **Caution**: If the signature of ``f`` cannot be inspected, currying fails, raising ``ValueError``, like ``inspect.signature`` does. This may happen with builtins such as ``list.append``, ``operator.add``, ``print``, or ``range``, depending on which version of Python you have (and whether CPython or PyPy3).
10101012
- **Added in v0.15.0.** `partial` with run-time type checking, which helps a lot with fail-fast in code that uses partial application. This function type-checks arguments against type annotations, then delegates to `functools.partial`. Supports `unpythonic`'s `@generic` and `@typed` functions, too.
10111013
- `composel`, `composer`: both left-to-right and right-to-left function composition, to help readability.
10121014
- 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.
@@ -1090,7 +1092,7 @@ assert myzipr((1, 2, 3), (4, 5, 6), (7, 8)) == ((2, 5, 8), (1, 4, 7))
10901092
assert tuple(zipr((1, 2, 3), (4, 5, 6), (7, 8))) == ((2, 5, 8), (1, 4, 7)) # zip first
10911093
assert tuple(rzip((1, 2, 3), (4, 5, 6), (7, 8))) == ((3, 6, 8), (2, 5, 7)) # reverse first
10921094

1093-
# curry with passthrough on the right
1095+
# curry with passthrough (positionals passed through on the right)
10941096
# final result is a tuple of the result(s) and the leftover args
10951097
double = lambda x: 2 * x
10961098
with dyn.let(curry_context=["whatever"]): # set a context to allow passthrough to the top level
@@ -1134,7 +1136,7 @@ Yet another way to write ``map_one`` is:
11341136
mymap = lambda f: curry(foldr, composer(cons, curry(f)), nil)
11351137
```
11361138

1137-
The curried ``f`` uses up one argument (provided it is a one-argument function!), and the second argument is passed through on the right; this two-tuple then ends up as the arguments to ``cons``.
1139+
The curried ``f`` uses up one argument (provided it is a one-argument function!), and the second argument is passed through on the right; these two values then end up as the arguments to ``cons``.
11381140

11391141
Using a currying compose function (name suffixed with ``c``), the inner curry can be dropped:
11401142

@@ -1146,9 +1148,11 @@ assert curry(mymap, myadd, ll(1, 2, 3), ll(2, 4, 6)) == ll(3, 6, 9)
11461148

11471149
This is as close to ```(define (map f) (foldr (compose cons f) empty)``` (in ``#lang`` [``spicy``](https://github.com/Technologicat/spicy)) as we're gonna get in Python.
11481150
1149-
Notice how the last two versions accept multiple input iterables; this is thanks to currying ``f`` inside the composition. An element from each of the iterables is taken by the processing function ``f``. Being the last argument, ``acc`` is passed through on the right. The output from the processing function - one new item - and ``acc`` then become a two-tuple, passed into cons.
1151+
Notice how the last two versions accept multiple input iterables; this is thanks to currying ``f`` inside the composition. An element from each of the iterables is taken by the processing function ``f``. Being the last argument, ``acc`` is passed through on the right. The output from the processing function - one new item - and ``acc`` then become two arguments, passed into cons.
11501152
1151-
Finally, keep in mind this exercise is intended as a feature demonstration. In production code, the builtin ``map`` is much better.
1153+
Finally, keep in mind this exercise is intended as a feature demonstration. In production code, the builtin ``map`` is much better. It produces a lazy iterable, and does not care which kind of actual data structure the items will be stored in (once computed).
1154+
1155+
The example we have here evaluates all items immediately, and specifically produces a linked list. It's just a nice example of function composition involving incompatible arities, thus demonstrating the kind of situation where the passthrough feature of `curry` is useful. It is taken from a paper by [John Hughes (1984)](https://www.cse.chalmers.se/~rjmh/Papers/whyfp.html).
11521156
11531157
11541158
#### ``curry`` and reduction rules
@@ -1172,15 +1176,20 @@ it means the following. Let ``m1`` and ``m2`` be the minimum and maximum positio
11721176
- If ``n < m1``, partially apply ``f`` to the given arguments, yielding a new function with smaller ``m1``, ``m2``. Then curry the result and return it.
11731177
- Internally we stack ``functools.partial`` applications, but there will be only one ``curried`` wrapper no matter how many invocations are used to build up arguments before ``f`` eventually gets called.
11741178
1175-
As of v0.15.0, the actual algorithm by which `curry` decides what to do, in the presence of kwargs and `@generic` functions, is:
1179+
As of v0.15.0, the actual algorithm by which `curry` decides what to do, in the presence of kwargs, `@generic` functions, and `Values` multiple-return-values, is:
11761180
11771181
- If `f` is **not** `@generic` or `@typed`:
11781182
- Compute parameter bindings of the args and kwargs collected so far, against the call signature of `f`.
11791183
- Note we keep track of which arguments were passed positionally and which by name. To avoid subtle errors, they are eventually passed to `f` the same way they were passed to `curry`. (Positional args are passed positionally, and kwargs are passed by name.)
11801184
- If there are no unbound parameters, and no args/kwargs are left over, we have an exact match. Call `f` and return its result, like a normal function call.
11811185
- Any sequence of curried calls that ends up binding all parameters of `f` triggers the call.
11821186
- As before, beware when working with variadic functions. Particularly, keep in mind that `*args` matches **zero or more** positional arguments (as the [Kleene star](https://en.wikipedia.org/wiki/Kleene_star)-ish notation indeed suggests).
1183-
- If there are no unbound parameters, but there are args/kwargs left over, arrange passthrough for the leftover args/kwargs (that were rejected by the call signature of `f`), and call `f`. If the result is a callable, curry it, and recurse. Else form a tuple... (as above).
1187+
- If there are no unbound parameters, but there are args/kwargs left over, arrange passthrough for the leftover args/kwargs (that were rejected by the call signature of `f`), and call `f`. Any leftover positional arguments are passed through **on the right**.
1188+
- Merge the return value of `f` with the leftover args/kwargs, thus forming updated leftover args/kwargs.
1189+
- If the return value of `f` is a `Values`: prepend positional return values into the leftover args (i.e. insert them **on the left**), and update the leftover kwargs with the named return values. (I.e. a key name conflict causes an overwrite in the leftover kwargs.)
1190+
- Else: there is just one positional return value. Prepend it to the leftover args.
1191+
- If the first positional return value is a callable: remove it from the leftover args, curry it, and recurse with the (updated) leftover args/kwargs.
1192+
- Else: form a `Values` from the leftover args/kwargs, and return it. (This return goes to the next outer curry context, or at the top level, to the original caller.)
11841193
- If neither of the above match, we know there is at least one unbound parameter, i.e. we have a partial match. Keep currying.
11851194
- If `f` is `@generic` or `@typed`:
11861195
- Iterate over multimethods registered on `f`, **up to three times**.

0 commit comments

Comments
 (0)