Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Piping syntax #612

Open
sbarzowski opened this issue Mar 2, 2019 · 13 comments
Open

Piping syntax #612

sbarzowski opened this issue Mar 2, 2019 · 13 comments

Comments

@sbarzowski
Copy link
Collaborator

I don't think there is any rush, I just wanted to get this idea out of my head.

The proposal has two parts:

  1. Forward and backward piping operators

a |> b = b(a) // forward piping
a <| b = a(b) // backward piping

The arrow points in which direction the value flows. The forward piping allows programming in UNIX-pipes style - allows cleanly doing multi-step processing without defining too many helper functions.

Backward piping is going to be useful with more "functional style" programs - it allows avoiding some parentheses (pretty much like Haskell's $).

  1. In Jsonnet curried functions are not that common, so in order to make the use of (1) practical, there needs to be a way to choose an argument for piping. I think I came up with a nice syntax.

I'll start with an example:

["foo", "FOO", "bar"] |> std.map(capitalize, _) |> std.sort | std.uniq

So the idea is that instead of an argument _ can be "passed" - a hole where a value can be put. The _ is interpreted this way only when it is passed as an argument. No complex expressions involving it are allowed. f(2 + _) is not allowed.

So f(a, b, c, _, d) = function(x) f(a, b, c, x, d).

It's unambiguous which expression is wrapped in a function - it's always just the function application.

It would be possible to also do things like this:

arrayOfStrings |> std.filter(std.startsWith(_, "prefix"), _) |> something

The issue is that _ is a valid identifier. It's unfortunate. We can either find a new symbol or interpret it this way only when there is no local variable named that. This can be handled during desugaring, though it's going to be quite annoying. We should also discourage using _ (or anything starting with _ for future proofing) if we go with that - possibly using Linter.

@sparkprime
Copy link
Member

I think @hausdorff proposed the same thing before, even with the same syntax

@sbarzowski
Copy link
Collaborator Author

Yeah, I think it was pretty much the same thing. From what I remember the approach to partial application was different (from what I remember it was something like one more argument was implicitly passed, and it couldn't be used without "piping").

I can't find the issue or anything, though (did the discussion take place only on Slack?).

@sbarzowski
Copy link
Collaborator Author

I think that it's better to have just the "forward piping" version (it's better not to provide two competing ways of doing the same thing). I don't see any case where backward piping (a'la Haskell's $) provides a clear advantage.

@sbarzowski
Copy link
Collaborator Author

Hmmm... actually backward piping could be useful for things like std.trace.

@sh0rez
Copy link

sh0rez commented Aug 24, 2020

(or anything starting with _ for future proofing) if we go with that - possibly using Linter.

Would that imply object keys like _config are discouraged? These are quite widely used for "internal" hidden fields that are supposed to be mixed into

@sbarzowski
Copy link
Collaborator Author

First, this was just about locals. Object fields are a completely different story. They don't even have to be valid identifiers.

It may still be to strong. One concrete thing I was I was thinking about was having something like _1, _2 meaning first, second argument etc.

@dasch
Copy link

dasch commented Dec 19, 2022

Any updates on this? I've found myself wanting this syntax quite a lot. Furthermore, I think there's a detail that could make it even nicer: having the null value have special meaning in a pipeline.

Let's say the pipe function implements piping. I'd love to see this rough implementation:

local pipe(input, f) = if f == null then input else f(input);

Why would that be nice? Consider this:

input
  |> if config.capitalize then capitalize(_)
  |> if config.revert then revert(_)
  ...

I often find myself conditionally applying functions; without this, you'd still need to branch out and use temporary variables to share a common "prefix" of a pipeline. In the above case, you'd need to do something like this:

local a = if config.capitalize then capitalize(input) else input;
local b = if config.revert then revert(a) else a;
...

@sbarzowski
Copy link
Collaborator Author

Yeah, having null == id in this context seems to be quite convenient.

This feature is not strictly required for this flow. You can have:

local id(x) = x; // could also be included in stdlib;

input
  |> if config.capitalize then capitalize else id
  |> if config.revert then revert else id

@sbarzowski
Copy link
Collaborator Author

And there has been no progress. If someone wants to pick it up, it would be a very welcome addition. I'll be happy to help in case of any questions.

CertainLach added a commit to CertainLach/jrsonnet that referenced this issue Jan 29, 2023
google/jsonnet#612
Signed-off-by: Yaroslav Bolyukin <iam@lach.pw>
@CertainLach
Copy link
Contributor

Implemented feature as described in this thread in jrsonnet (in branch, not in release), but without

So the idea is that instead of an argument _ can be "passed" - a hole where a value can be put. The _ is interpreted this way only when it is passed as an argument. No complex expressions involving it are allowed. f(2 + _) is not allowed.

limitation, as I'm probably not sure, why this limitation is needed. Please point out if I misunderstood proposed semantics :D

I think the proposed semantics is quite vague, I do not like that both value |> mapper and value |> mapper(_) can be used, why not choose one of those syntaxes?

If anyone wants to try, jrsonnet with this feature can be installed using cargo install --git https://github.com/CertainLach/jrsonnet --branch feature/piping, call example: jrsonnet -e '2 |> _ * 2 |> _ + 1'

@sbarzowski
Copy link
Collaborator Author

limitation, as I'm probably not sure, why this limitation is needed.

To avoid ambiguity when nesting. How do you know if f(g(_)) is a partial application of g or of f?

Also, the implementation could be simpler, depending on internal representations.

Generally, I think it's better to err on the side of minimalism in these things – adding is much easier than removing.

@sbarzowski
Copy link
Collaborator Author

I think the proposed semantics is quite vague, I do not like that both value |> mapper and value |> mapper(_) can be used, why not choose one of those syntaxes?

My suggestion was that mapper(_) can be used in any context a function is expected, not just for pipes. So e.g. you could also write something like std.filter(greater_than(3, _), my_arr).

Then |> accepts any single-argument function as the mapper. It's just an operator, without any special syntax on its own.

@CertainLach
Copy link
Contributor

My suggestion was that mapper(_) can be used in any context a function is expected, not just for pipes. So e.g. you could also write something like std.filter(greater_than(3, _), my_arr).

Aha! Makes sense. In this case, something is certainly needed for multiple arguments.

My current implementation only has special handling for |>, desugaring (roughly) a |> b |> c as local _ = (local _ = a; b); c: https://github.com/CertainLach/jrsonnet/blob/8b201a56525fa9c36c32185d9680a9236e31aee0/crates/jrsonnet-evaluator/src/evaluate/mod.rs#L646-L658

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants