Skip to content

Commit

Permalink
Add walrus partial in pipes
Browse files Browse the repository at this point in the history
Resolves   #823.
  • Loading branch information
evhub committed Jan 20, 2024
1 parent 6517559 commit e57c05c
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 21 deletions.
11 changes: 7 additions & 4 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,9 +688,10 @@ Coconut uses pipe operators for pipeline-style function application. All the ope

The None-aware pipe operators here are equivalent to a [monadic bind](https://en.wikipedia.org/wiki/Monad_(functional_programming)) treating the object as a `Maybe` monad composed of either `None` or the given object. Thus, `x |?> f` is equivalent to `None if x is None else f(x)`. Note that only the object being piped, not the function being piped into, may be `None` for `None`-aware pipes.

For working with `async` functions in pipes, all non-starred pipes support piping into `await` to await the awaitable piped into them, such that `x |> await` is equivalent to `await x`.

Additionally, all pipe operators support a lambda as the last argument, despite lambdas having a lower precedence. Thus, `a |> x => b |> c` is equivalent to `a |> (x => b |> c)`, not `a |> (x => b) |> c`.
Additionally, some special syntax constructs are only available in pipes to enable doing as many operations as possible via pipes if so desired:
* For working with `async` functions in pipes, all non-starred pipes support piping into `await` to await the awaitable piped into them, such that `x |> await` is equivalent to `await x`.
* All non-starred pipes support piping into `(<name> := .)` (mirroring the syntax for [operator implicit partials](#implicit-partial-application)) to assign the piped in item to `<name>`.
* All pipe operators support a lambda as the last argument, despite lambdas having a lower precedence. Thus, `a |> x => b |> c` is equivalent to `a |> (x => b |> c)`, not `a |> (x => b) |> c`.

_Note: To visually spread operations across several lines, just use [parenthetical continuation](#enhanced-parenthetical-continuation)._

Expand Down Expand Up @@ -1766,6 +1767,8 @@ _Deprecated: if the deprecated `->` is used in place of `=>`, then return type a

Coconut uses a simple operator function short-hand: surround an operator with parentheses to retrieve its function. Similarly to iterator comprehensions, if the operator function is the only argument to a function, the parentheses of the function call can also serve as the parentheses for the operator function.

All operator functions also support [implicit partial application](#implicit-partial-application), e.g. `(. + 1)` is equivalent to `(=> _ + 1)`.

##### Rationale

A very common thing to do in functional programming is to make use of function versions of built-in operators: currying them, composing them, and piping them. To make this easy, Coconut provides a short-hand syntax to access operator functions.
Expand Down Expand Up @@ -2486,7 +2489,7 @@ where `<arg>` is defined as
```
where `<name>` is the name of the function, `<cond>` is an optional additional check, `<body>` is the body of the function, `<pattern>` is defined by Coconut's [`match` statement](#match), `<default>` is the optional default if no argument is passed, and `<return_type>` is the optional return type annotation (note that argument type annotations are not supported for pattern-matching functions). The `match` keyword at the beginning is optional, but is sometimes necessary to disambiguate pattern-matching function definition from normal function definition, since Python function definition will always take precedence. Note that the `async` and `match` keywords can be in any order.

If `<pattern>` has a variable name (via any variable binding that binds the entire pattern), the resulting pattern-matching function will support keyword arguments using that variable name.
If `<pattern>` has a variable name (via any variable binding that binds the entire pattern, e.g. `x` in `int(x)` or `[a, b] as x`), the resulting pattern-matching function will support keyword arguments using that variable name.

In addition to supporting pattern-matching in their arguments, pattern-matching function definitions also have a couple of notable differences compared to Python functions. Specifically:
- If pattern-matching function definition fails, it will raise a [`MatchError`](#matcherror) (just like [destructuring assignment](#destructuring-assignment)) instead of a `TypeError`.
Expand Down
51 changes: 37 additions & 14 deletions coconut/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2445,7 +2445,8 @@ def proc_funcdef(self, original, loc, decorators, funcdef, is_async, in_method,
raise self.make_err(
CoconutTargetError,
"async function definition requires a specific target",
original, loc,
original,
loc,
target="sys",
)
elif self.target_info >= (3, 5):
Expand All @@ -2456,7 +2457,8 @@ def proc_funcdef(self, original, loc, decorators, funcdef, is_async, in_method,
raise self.make_err(
CoconutTargetError,
"found Python 3.6 async generator (Coconut can only backport async generators as far back as 3.5)",
original, loc,
original,
loc,
target="35",
)
else:
Expand Down Expand Up @@ -2815,16 +2817,18 @@ def function_call_handle(self, loc, tokens):
"""Enforce properly ordered function parameters."""
return "(" + join_args(*self.split_function_call(tokens, loc)) + ")"

def pipe_item_split(self, tokens, loc):
def pipe_item_split(self, original, loc, tokens):
"""Process a pipe item, which could be a partial, an attribute access, a method call, or an expression.
Return (type, split) where split is:
- (expr,) for expression
- (func, pos_args, kwd_args) for partial
- (name, args) for attr/method
- (attr, [(op, args)]) for itemgetter
- (op, arg) for right op partial
- (op, arg) for right arr concat partial
Return (type, split) where split is, for each type:
- expr: (expr,)
- partial: (func, pos_args, kwd_args)
- attrgetter: (name, args)
- itemgetter: (attr, [(op, args)]) for itemgetter
- right op partial: (op, arg)
- right arr concat partial: (op, arg)
- await: ()
- namedexpr: (varname,)
"""
# list implies artificial tokens, which must be expr
if isinstance(tokens, list) or "expr" in tokens:
Expand Down Expand Up @@ -2868,7 +2872,18 @@ def pipe_item_split(self, tokens, loc):
raise CoconutInternalException("invalid arr concat partial tokens in pipe_item", inner_toks)
elif "await" in tokens:
internal_assert(len(tokens) == 1 and tokens[0] == "await", "invalid await pipe item tokens", tokens)
return "await", []
return "await", ()
elif "namedexpr" in tokens:
if self.target_info < (3, 8):
raise self.make_err(
CoconutTargetError,
"named expression partial in pipe only supported for targets 3.8+",
original,
loc,
target="38",
)
varname, = tokens
return "namedexpr", (varname,)
else:
raise CoconutInternalException("invalid pipe item tokens", tokens)

Expand All @@ -2882,7 +2897,7 @@ def pipe_handle(self, original, loc, tokens, **kwargs):
return item

# we've only been given one operand, so we can't do any optimization, so just produce the standard object
name, split_item = self.pipe_item_split(item, loc)
name, split_item = self.pipe_item_split(original, loc, item)
if name == "expr":
expr, = split_item
return expr
Expand All @@ -2899,6 +2914,8 @@ def pipe_handle(self, original, loc, tokens, **kwargs):
return partial_arr_concat_handle(item)
elif name == "await":
raise CoconutDeferredSyntaxError("await in pipe must have something piped into it", loc)
elif name == "namedexpr":
raise CoconutDeferredSyntaxError("named expression partial in pipe must have something piped into it", loc)
else:
raise CoconutInternalException("invalid split pipe item", split_item)

Expand Down Expand Up @@ -2929,7 +2946,7 @@ def pipe_handle(self, original, loc, tokens, **kwargs):

elif direction == "forwards":
# if this is an implicit partial, we have something to apply it to, so optimize it
name, split_item = self.pipe_item_split(item, loc)
name, split_item = self.pipe_item_split(original, loc, item)
subexpr = self.pipe_handle(original, loc, tokens)

if name == "expr":
Expand Down Expand Up @@ -2976,6 +2993,11 @@ def pipe_handle(self, original, loc, tokens, **kwargs):
if stars:
raise CoconutDeferredSyntaxError("cannot star pipe into await", loc)
return self.await_expr_handle(original, loc, [subexpr])
elif name == "namedexpr":
if stars:
raise CoconutDeferredSyntaxError("cannot star pipe into named expression partial", loc)
varname, = split_item
return "({varname} := {item})".format(varname=varname, item=subexpr)
else:
raise CoconutInternalException("invalid split pipe item", split_item)

Expand Down Expand Up @@ -3952,7 +3974,8 @@ def await_expr_handle(self, original, loc, tokens):
raise self.make_err(
CoconutTargetError,
"await requires a specific target",
original, loc,
original,
loc,
target="sys",
)
elif self.target_info >= (3, 5):
Expand Down
10 changes: 8 additions & 2 deletions coconut/compiler/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,9 @@ class Grammar(object):
back_none_dubstar_pipe,
use_adaptive=False,
)
pipe_namedexpr_partial = lparen.suppress() + setname + (colon_eq + dot + rparen).suppress()

# make sure to keep these three definitions in sync
pipe_item = (
# we need the pipe_op since any of the atoms could otherwise be the start of an expression
labeled_group(keyword("await"), "await") + pipe_op
Expand All @@ -1574,6 +1577,7 @@ class Grammar(object):
| labeled_group(attrgetter_atom_tokens, "attrgetter") + pipe_op
| labeled_group(partial_op_atom_tokens, "op partial") + pipe_op
| labeled_group(partial_arr_concat_tokens, "arr concat partial") + pipe_op
| labeled_group(pipe_namedexpr_partial, "namedexpr") + pipe_op
# expr must come at end
| labeled_group(comp_pipe_expr, "expr") + pipe_op
)
Expand All @@ -1585,23 +1589,25 @@ class Grammar(object):
| labeled_group(attrgetter_atom_tokens, "attrgetter") + end_simple_stmt_item
| labeled_group(partial_op_atom_tokens, "op partial") + end_simple_stmt_item
| labeled_group(partial_arr_concat_tokens, "arr concat partial") + end_simple_stmt_item
| labeled_group(pipe_namedexpr_partial, "namedexpr") + end_simple_stmt_item
)
last_pipe_item = Group(
lambdef("expr")
# we need longest here because there's no following pipe_op we can use as above
| longest(
keyword("await")("await"),
partial_atom_tokens("partial"),
itemgetter_atom_tokens("itemgetter"),
attrgetter_atom_tokens("attrgetter"),
partial_atom_tokens("partial"),
partial_op_atom_tokens("op partial"),
partial_arr_concat_tokens("arr concat partial"),
pipe_namedexpr_partial("namedexpr"),
comp_pipe_expr("expr"),
)
)

normal_pipe_expr = Forward()
normal_pipe_expr_tokens = OneOrMore(pipe_item) + last_pipe_item

pipe_expr = (
comp_pipe_expr + ~pipe_op
| normal_pipe_expr
Expand Down
2 changes: 1 addition & 1 deletion coconut/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
VERSION = "3.0.4"
VERSION_NAME = None
# False for release, int >= 1 for develop
DEVELOP = 17
DEVELOP = 18
ALPHA = False # for pre releases rather than post releases

assert DEVELOP is False or DEVELOP >= 1, "DEVELOP must be False or an int >= 1"
Expand Down
3 changes: 3 additions & 0 deletions coconut/tests/src/cocotest/target_38/py38_test.coco
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ def py38_test() -> bool:
assert a == 3 == b
def f(x: int, /, y: int) -> int = x + y
assert f(1, y=2) == 3
assert 10 |> (x := .) == 10 == x
assert 10 |> (x := .) |> (. + 1) == 11
assert x == 10
return True

0 comments on commit e57c05c

Please sign in to comment.