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:
piegames 2024-07-11 10:49:15 +02:00
parent 6fdb47f0b2
commit 28ae24f3f7
8 changed files with 126 additions and 0 deletions

View file

@ -103,6 +103,11 @@ midnightveil:
ncfavier:
github: ncfavier
piegames:
display_name: piegames
forgejo: piegames
github: piegamesde
puck:
display_name: puck
forgejo: puck

View 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.

View file

@ -26,6 +26,8 @@
| Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 |
| Logical disjunction (`OR`) | *bool* <code>\|\|</code> *bool* | left | 13 |
| [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
[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*.
[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

View file

@ -434,6 +434,8 @@ struct op {
struct and_ : _op<TAO_PEGTL_STRING("&&"), 12> {};
struct or_ : _op<TAO_PEGTL_STRING("||"), 13> {};
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 {
@ -521,6 +523,7 @@ struct _expr {
app
> {};
/* Order matters here. The order is the parsing order, not the precedence order: '<=' must be parsed before '<'. */
struct _binary_operator : sor<
operator_<op::implies>,
operator_<op::update>,
@ -529,6 +532,8 @@ struct _expr {
operator_<op::minus>,
operator_<op::mul>,
operator_<op::div>,
operator_<op::pipe_right>,
operator_<op::pipe_left>,
operator_<op::less_eq>,
operator_<op::greater_eq>,
operator_<op::less>,
@ -649,6 +654,8 @@ struct operator_semantics {
grammar::op::minus,
grammar::op::mul,
grammar::op::div,
grammar::op::pipe_right,
grammar::op::pipe_left,
has_attr
> op;
};

View file

@ -113,6 +113,29 @@ struct ExprState
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)
{
return call(pos, state.s.lessThan, !less);
@ -162,6 +185,8 @@ struct ExprState
[&] (Op::concat) { return applyBinary<ExprOpConcatLists>(pos); },
[&] (has_attr & a) { return applyUnary<ExprOpHasAttr>(std::move(a.path)); },
[&] (Op::unary_minus) { return negate(pos, state); },
[&] (Op::pipe_right) { return pipe(pos, state, true); },
[&] (Op::pipe_left) { return pipe(pos, state); },
})(op)
};
}

View file

@ -166,6 +166,16 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails
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,
.name = "fetch-closure",

View file

@ -21,6 +21,7 @@ enum struct ExperimentalFeature
NixCommand,
RecursiveNix,
NoUrlLiterals,
PipeOperator,
FetchClosure,
ReplFlake,
AutoAllocateUids,

View file

@ -210,4 +210,40 @@ namespace nix {
TEST_F(TrivialExpressionTest, orCantBeUsed) {
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 */