libexpr: Add experimental pipe operator
The |> operator is a reverse function operator with low binding strength
to replace lib.pipe. Implements RFC 148, see the RFC text for more
details. Closes #438.
Change-Id: I21df66e8014e0d4dd9753dd038560a2b0b7fd805
This commit is contained in:
parent
6fdb47f0b2
commit
28ae24f3f7
|
@ -103,6 +103,11 @@ midnightveil:
|
||||||
ncfavier:
|
ncfavier:
|
||||||
github: ncfavier
|
github: ncfavier
|
||||||
|
|
||||||
|
piegames:
|
||||||
|
display_name: piegames
|
||||||
|
forgejo: piegames
|
||||||
|
github: piegamesde
|
||||||
|
|
||||||
puck:
|
puck:
|
||||||
display_name: puck
|
display_name: puck
|
||||||
forgejo: puck
|
forgejo: puck
|
||||||
|
|
10
doc/manual/rl-next/pipe-operator.md
Normal file
10
doc/manual/rl-next/pipe-operator.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
synopsis: Pipe operator `|>` (experimental)
|
||||||
|
issues: [fj#438]
|
||||||
|
cls: [1654]
|
||||||
|
category: Features
|
||||||
|
credits: [piegames, horrors]
|
||||||
|
---
|
||||||
|
|
||||||
|
Implementation of the pipe operator (`|>`) in the language as described in [RFC 148](https://github.com/NixOS/rfcs/pull/148).
|
||||||
|
The feature is still marked experimental, enable `--extra-experimental-features pipe-operator` to use it.
|
|
@ -26,6 +26,8 @@
|
||||||
| Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 |
|
| Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 |
|
||||||
| Logical disjunction (`OR`) | *bool* <code>\|\|</code> *bool* | left | 13 |
|
| Logical disjunction (`OR`) | *bool* <code>\|\|</code> *bool* | left | 13 |
|
||||||
| [Logical implication] | *bool* `->` *bool* | none | 14 |
|
| [Logical implication] | *bool* `->` *bool* | none | 14 |
|
||||||
|
| \[Experimental\] [Function piping] | *expr* |> *func* | left | 15 |
|
||||||
|
| \[Experimental\] [Function piping] | *expr* <| *func* | right | 16 |
|
||||||
|
|
||||||
[string]: ./values.md#type-string
|
[string]: ./values.md#type-string
|
||||||
[path]: ./values.md#type-path
|
[path]: ./values.md#type-path
|
||||||
|
@ -215,3 +217,33 @@ nix-repl> let f = x: 1; s = { func = f; }; in [ (f == f) (s == s) ]
|
||||||
Equivalent to `!`*b1* `||` *b2*.
|
Equivalent to `!`*b1* `||` *b2*.
|
||||||
|
|
||||||
[Logical implication]: #logical-implication
|
[Logical implication]: #logical-implication
|
||||||
|
|
||||||
|
## \[Experimental\] Function piping
|
||||||
|
|
||||||
|
*This language feature is still experimental and may change at any time. Enable `--extra-experimental-features pipe-operator` to use it.*
|
||||||
|
|
||||||
|
Pipes are a dedicated operator for function application, but with reverse order and a lower binding strength.
|
||||||
|
This allows you to chain function calls together in way that is more natural to read and requires less parentheses.
|
||||||
|
|
||||||
|
`a |> f b |> g` is equivalent to `g (f b a)`.
|
||||||
|
`g <| f b <| a` is equivalent to `g (f b a)`.
|
||||||
|
|
||||||
|
Example code snippet:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
defaultPrefsFile = defaultPrefs
|
||||||
|
|> lib.mapAttrsToList (
|
||||||
|
key: value: ''
|
||||||
|
// ${value.reason}
|
||||||
|
pref("${key}", ${builtins.toJSON value.value});
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|> lib.concatStringsSep "\n"
|
||||||
|
|> pkgs.writeText "nixos-default-prefs.js";
|
||||||
|
```
|
||||||
|
|
||||||
|
Note how `mapAttrsToList` is called with two arguments (the lambda and `defaultPrefs`),
|
||||||
|
but moving the last argument in front of the rest improves the reading flow.
|
||||||
|
This is common for functions with long first argument, including all `map`-like functions.
|
||||||
|
|
||||||
|
[Function piping]: #experimental-function-piping
|
||||||
|
|
|
@ -434,6 +434,8 @@ struct op {
|
||||||
struct and_ : _op<TAO_PEGTL_STRING("&&"), 12> {};
|
struct and_ : _op<TAO_PEGTL_STRING("&&"), 12> {};
|
||||||
struct or_ : _op<TAO_PEGTL_STRING("||"), 13> {};
|
struct or_ : _op<TAO_PEGTL_STRING("||"), 13> {};
|
||||||
struct implies : _op<TAO_PEGTL_STRING("->"), 14, kind::rightAssoc> {};
|
struct implies : _op<TAO_PEGTL_STRING("->"), 14, kind::rightAssoc> {};
|
||||||
|
struct pipe_right : _op<TAO_PEGTL_STRING("|>"), 15> {};
|
||||||
|
struct pipe_left : _op<TAO_PEGTL_STRING("<|"), 16, kind::rightAssoc> {};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct _expr {
|
struct _expr {
|
||||||
|
@ -521,6 +523,7 @@ struct _expr {
|
||||||
app
|
app
|
||||||
> {};
|
> {};
|
||||||
|
|
||||||
|
/* Order matters here. The order is the parsing order, not the precedence order: '<=' must be parsed before '<'. */
|
||||||
struct _binary_operator : sor<
|
struct _binary_operator : sor<
|
||||||
operator_<op::implies>,
|
operator_<op::implies>,
|
||||||
operator_<op::update>,
|
operator_<op::update>,
|
||||||
|
@ -529,6 +532,8 @@ struct _expr {
|
||||||
operator_<op::minus>,
|
operator_<op::minus>,
|
||||||
operator_<op::mul>,
|
operator_<op::mul>,
|
||||||
operator_<op::div>,
|
operator_<op::div>,
|
||||||
|
operator_<op::pipe_right>,
|
||||||
|
operator_<op::pipe_left>,
|
||||||
operator_<op::less_eq>,
|
operator_<op::less_eq>,
|
||||||
operator_<op::greater_eq>,
|
operator_<op::greater_eq>,
|
||||||
operator_<op::less>,
|
operator_<op::less>,
|
||||||
|
@ -649,6 +654,8 @@ struct operator_semantics {
|
||||||
grammar::op::minus,
|
grammar::op::minus,
|
||||||
grammar::op::mul,
|
grammar::op::mul,
|
||||||
grammar::op::div,
|
grammar::op::div,
|
||||||
|
grammar::op::pipe_right,
|
||||||
|
grammar::op::pipe_left,
|
||||||
has_attr
|
has_attr
|
||||||
> op;
|
> op;
|
||||||
};
|
};
|
||||||
|
|
|
@ -113,6 +113,29 @@ struct ExprState
|
||||||
return std::make_unique<ExprCall>(pos, std::make_unique<ExprVar>(fn), std::move(args));
|
return std::make_unique<ExprCall>(pos, std::make_unique<ExprVar>(fn), std::move(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Expr> pipe(PosIdx pos, State & state, bool flip = false)
|
||||||
|
{
|
||||||
|
if (!state.xpSettings.isEnabled(Xp::PipeOperator))
|
||||||
|
throw ParseError({
|
||||||
|
.msg = HintFmt("Pipe operator is disabled"),
|
||||||
|
.pos = state.positions[pos]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reverse the order compared to normal function application: arg |> fn
|
||||||
|
std::unique_ptr<Expr> fn, arg;
|
||||||
|
if (flip) {
|
||||||
|
fn = popExprOnly();
|
||||||
|
arg = popExprOnly();
|
||||||
|
} else {
|
||||||
|
arg = popExprOnly();
|
||||||
|
fn = popExprOnly();
|
||||||
|
}
|
||||||
|
std::vector<std::unique_ptr<Expr>> args{1};
|
||||||
|
args[0] = std::move(arg);
|
||||||
|
|
||||||
|
return std::make_unique<ExprCall>(pos, std::move(fn), std::move(args));
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Expr> order(PosIdx pos, bool less, State & state)
|
std::unique_ptr<Expr> order(PosIdx pos, bool less, State & state)
|
||||||
{
|
{
|
||||||
return call(pos, state.s.lessThan, !less);
|
return call(pos, state.s.lessThan, !less);
|
||||||
|
@ -162,6 +185,8 @@ struct ExprState
|
||||||
[&] (Op::concat) { return applyBinary<ExprOpConcatLists>(pos); },
|
[&] (Op::concat) { return applyBinary<ExprOpConcatLists>(pos); },
|
||||||
[&] (has_attr & a) { return applyUnary<ExprOpHasAttr>(std::move(a.path)); },
|
[&] (has_attr & a) { return applyUnary<ExprOpHasAttr>(std::move(a.path)); },
|
||||||
[&] (Op::unary_minus) { return negate(pos, state); },
|
[&] (Op::unary_minus) { return negate(pos, state); },
|
||||||
|
[&] (Op::pipe_right) { return pipe(pos, state, true); },
|
||||||
|
[&] (Op::pipe_left) { return pipe(pos, state); },
|
||||||
})(op)
|
})(op)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,16 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails
|
||||||
may confuse external tooling.
|
may confuse external tooling.
|
||||||
)",
|
)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.tag = Xp::PipeOperator,
|
||||||
|
.name = "pipe-operator",
|
||||||
|
.description = R"(
|
||||||
|
Enable new operators for function application to "pipe" arguments through a chain of functions similar to `lib.pipe`.
|
||||||
|
This implementation is based on Nix [RFC 148](https://github.com/NixOS/rfcs/pull/148).
|
||||||
|
|
||||||
|
Tracking issue: https://git.lix.systems/lix-project/lix/issues/438
|
||||||
|
)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.tag = Xp::FetchClosure,
|
.tag = Xp::FetchClosure,
|
||||||
.name = "fetch-closure",
|
.name = "fetch-closure",
|
||||||
|
|
|
@ -21,6 +21,7 @@ enum struct ExperimentalFeature
|
||||||
NixCommand,
|
NixCommand,
|
||||||
RecursiveNix,
|
RecursiveNix,
|
||||||
NoUrlLiterals,
|
NoUrlLiterals,
|
||||||
|
PipeOperator,
|
||||||
FetchClosure,
|
FetchClosure,
|
||||||
ReplFlake,
|
ReplFlake,
|
||||||
AutoAllocateUids,
|
AutoAllocateUids,
|
||||||
|
|
|
@ -210,4 +210,40 @@ namespace nix {
|
||||||
TEST_F(TrivialExpressionTest, orCantBeUsed) {
|
TEST_F(TrivialExpressionTest, orCantBeUsed) {
|
||||||
ASSERT_THROW(eval("let or = 1; in or"), Error);
|
ASSERT_THROW(eval("let or = 1; in or"), Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pipes are gated behind an experimental feature flag
|
||||||
|
TEST_F(TrivialExpressionTest, pipeDisabled) {
|
||||||
|
ASSERT_THROW(eval("let add = l: r: l + r; in ''a'' |> add ''b''"), Error);
|
||||||
|
ASSERT_THROW(eval("let add = l: r: l + r; in ''a'' <| add ''b''"), Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TrivialExpressionTest, pipeRight) {
|
||||||
|
ExperimentalFeatureSettings mockXpSettings;
|
||||||
|
mockXpSettings.set("experimental-features", "pipe-operator");
|
||||||
|
|
||||||
|
auto v = eval("let add = l: r: l + r; in ''a'' |> add ''b''", true, mockXpSettings);
|
||||||
|
ASSERT_THAT(v, IsStringEq("ba"));
|
||||||
|
v = eval("let add = l: r: l + r; in ''a'' |> add ''b'' |> add ''c''", true, mockXpSettings);
|
||||||
|
ASSERT_THAT(v, IsStringEq("cba"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TrivialExpressionTest, pipeLeft) {
|
||||||
|
ExperimentalFeatureSettings mockXpSettings;
|
||||||
|
mockXpSettings.set("experimental-features", "pipe-operator");
|
||||||
|
|
||||||
|
auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b''", true, mockXpSettings);
|
||||||
|
ASSERT_THAT(v, IsStringEq("ab"));
|
||||||
|
v = eval("let add = l: r: l + r; in add ''a'' <| add ''b'' <| ''c''", true, mockXpSettings);
|
||||||
|
ASSERT_THAT(v, IsStringEq("abc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TrivialExpressionTest, pipeMixed) {
|
||||||
|
ExperimentalFeatureSettings mockXpSettings;
|
||||||
|
mockXpSettings.set("experimental-features", "pipe-operator");
|
||||||
|
|
||||||
|
auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b'' |> add ''c''", true, mockXpSettings);
|
||||||
|
ASSERT_THAT(v, IsStringEq("acb"));
|
||||||
|
v = eval("let add = l: r: l + r; in ''a'' |> add <| ''c''", true, mockXpSettings);
|
||||||
|
ASSERT_THAT(v, IsStringEq("ac"));
|
||||||
|
}
|
||||||
} /* namespace nix */
|
} /* namespace nix */
|
||||||
|
|
Loading…
Reference in a new issue