56 ms·
I'm personally someone who advocates for languages to keep their feature set small and shoot to achieve a finished feature set quickly. However. I would be ly
by bnchrch 1y ago
I'm personally someone who advocates for languages to keep their feature set small and shoot to achieve a finished feature set quickly.
However.
I would be lying if I didn't secretly wish that all languages adopted the `|>` syntax from Elixir.
```
params
|> Map.get("user")
|> create_user()
|> notify_admin()
```
- valenterry 1y agoI prefer Scala. You can write ``` params.get("user") |> create_user |> notify_admin ``` Even more concise and it doesn't even require a special language feature, it's just regular syntax of the language ( |> is a method like .get(...) so you could even write `params.get("user").|>(create_user) if you wanted to)
- elbasti 1y agoIn elixir, ```Map.get("user") |> create_user |> notify_admin ``` would aso be valid, standard elixir, just not idiomatic (parens are optional, but preferred in most cases, and one-line pipes are also frowned upon except for scripting).
- MaxBarraclough 1y agoWith the disclaimer that I don't know Elixir and haven't programmed with the pipeline operator before: I don't like that special () syntax. That syntax denotes application of the function without passing any arguments, but the whole point here is that an argument is being passed. It seems clearer to me to just put the pipeline operator and the name of the function that it's being used with. I don't see how it's unclear that application is being handled by the pipeline operator. Also, what if the function you want to use is returned by some nullary function? You couldn't just do |> getfunc(), as presumably the pipeline operator will interfere with the usual meaning of the parentheses and will try to pass something to getfunc. Would |> ( getfunc() ) work? This is the kind of problem that can arise when one language feature is permitted to change the ordinary behaviour of an existing feature in the name of convenience. (Unless of course I'm just missing something.)
- freehorse 1y agoI am also confused with such syntax of "passing as first argument" pipes. Having to write `x |> foo` instead of `x |> foo()` does not solve much, because you have the same lack of clarity if you need to pass a second argument. Ie `x |> foo(y)` in this case means `foo(x,y)`, but if `foo(y)` actually gives you a function to apply to `x` prob you should write `x |> foo(y)()` or `x |> (foo(y))()` then as I understand it? If that even makes sense in a language. In any case, you have the same issue as before, in different contexts `foo(y)` is interpreted differently. I just find this syntax too inconsistent and vague, and hence actually annoying. Which is why I prefer defining pipes as composition of functions which can then be applied to whatever data. Then eg one can write sth like `(|> foo1 foo2 (foo3) #(foo4 % y))` and know that foo1 and foo2 are references to functions, foo3 evaluates to another function, and when one needs more arguments in foo4 they have to explicitly state that. This gives another function, and there is no ambiguity here whatsoever.
- valenterry 1y ago> Having to write `x |> foo` instead of `x |> foo()` does not solve much, because you have the same lack of clarity if you need to pass a second argument That's actually true. In Scala that is not so nice, because then it becomes `x |> foo(_, arg2)` or, even worse, `x |> (param => foo(param, arg2))`. I have a few such cases in my sourcecode and I really don't like it. Haskell and PureScript do a much better job keeping the code clean in such cases.
- AlchemistCamp 1y agoIt would be silly to use a pipeline for x |> foo(). What's nice is being able to write: def main_loop(%Game{} = game) do game |> get_move() |> play_move() |> win_check() |> end_turn() end instead of the much harder to read: def main_loop(%Game{} = game) end_turn(win_check(play_move(get_move(game)))) end For an example with multiple parameters, this pipeline: schema |> order_by(^constraint) |> Repo.all() |> Repo.preload(preload_opts) would be identical to this: Repo.preload(Repo.all(order_by(schema, ^constraint)), preload_opts) To address your question above, > if `foo(y)` actually gives you a function to apply to `x` prob you should write `x |> foo(y)()` If foo(y) returned a function, then to call it with x, you would have to write foo(y).(x) or x |> foo(y).(), so the syntax around calling the anonymous function isn't affected by the pipe. Also, you're not generally going to be using pipelines with functions that return functions so much as with functions that return data which is then consumed as the first argument by the next function in the pipeline. See my previous comment on this thread for more on that point. There's no inconsistency or ambiguity in the pipeline operator's behavior. It's just syntactic sugar that's handy for making your code easier to read.
- valenterry 1y agoOh that's nice!
- agent281 1y agoIsn't it being a method call not quite equivalent? Are you able to define the method over arbitrary data types? In Elixir, it is just a macro so it applies to all functions. I'm only a Scala novice so I'm not sure how it would work there.
- valenterry 1y ago> Are you able to define the method over arbitrary data types? Yes exactly, which is why it is not equivalent. No macro needed here. In Scala 2 syntax: ``` implicit class AnyOps[A](private val a: A) extends AnyVal { def |>[B](f: A => B) = f(a) } ```
- Cyykratahk 1y agoWe might be able to cross one more language off your wishlist soon, Javascript is on the way to getting a pipeline operator, the proposal is currently at Stage 2 https://github.com/tc39/proposal-pipeline-operator https://github.com/tc39/proposal-pipeline-operator I'm very excited for it.
- zdragnar 1y agoI worry about "soon" here. I've been excited for this proposal for years now (8 maybe? I forget), and I'm not sure it'll ever actually get traction at this point.
- hoppp 1y agoCool I love it, but another thing we will need polyfills for...
- bobbylarrybobby 1y agoHow do you polyfill syntax?
- jononor 1y agoLetting your JS/TS compiler convert it into supported form. Not really a polyfill, but it allows to use new features in the source and still support older targets. This was done a lot when ES6 was new, I remember.
- zdragnar 1y agoPolyfills are for runtime behavior that can't be replicated with a simple syntax transformation, such as adding new functions to built-in objects like string.prototype contains or the Symbol constructor and prototype or custom elements. I haven't looked at the member properties bits but I suspect the pipeline syntax just needs the transform to be supported in build tools, rather than adding yet another polyfill.
- hathawsh 1y ago
- Symmetry 1y agoI feel like Haskell really missed a trick by having $ not go the other way, though it's trivial to make your own symbol that goes the other way.
- jose_zap 1y agoHaskell has & which goes the other way: users & map validate & catMaybes & mapM persist
- Symmetry 1y agoI guess I'm showing how long it's been since I was a student of Haskell then. Glad to see the addition!
- taolson 1y agoYes, `&` (reverse apply) is equivalent to `|>`, but it is interesting that there is no common operator for reversed compose `.`, so function compositions are still read right-to-left. In my programming language, I added `.>` as a reverse-compose operator, so pipelines of function compositions can also be read uniformly left-to-right, e.g. process = map validate .> catMaybes .> mapM persist
- 1-more 1y agoElm (written in Haskell) uses |> and <| for pipelining forwards and backwards, and function composition is >> and <<. These have made it into Haskell via nri-prelude https://hackage.haskell.org/package/nri-prelude https://hackage.haskell.org/package/nri-prelude (written by a company that uses a lot of Elm in order to make writing Haskell look more like writing Elm). There is also https://hackage.haskell.org/package/flow https://hackage.haskell.org/package/flow which uses .> and <. for function composition. EDIT: in no way do I want to claim the originality of these things in Elm or the Haskell package inspired by it. AFAIK |> came from F# but it could be miles earlier.
- shadytrees 1y agoMaybe not common, but there’s Control.Arrow.(>>>)
- jasperry 1y agoYes, a small feature set is important, and adding the functional-style pipe to languages that already have chaining with the dot seems to clutter up the design space. However, dot-chaining has the severe limitation that you can only pass to the first or "this" argument. Is there any language with a single feature that gives the best of both worlds?
- bnchrch 1y agoFWIW you can pass to other arguments than first in this syntax ``` params |> Map.get("user") |> create_user() |> (¬ify_admin("signup", &1)).() ``` or ``` params |> Map.get("user") |> create_user() |> (fn user -> notify_admin("signup", user) end).() ```
- Terr_ 1y agoBTW, there's a convenience macro of Kernel.then/2 [0] which IMO looks a little cleaner: params |> Map.get("user") |> create_user() |> then(¬ify_admin("signup", &1)) params |> Map.get("user") |> create_user() |> then(fn user -> notify_admin("signup", user) end) [0] https://hexdocs.pm/elixir/1.18.3/Kernel.html#then/2 https://hexdocs.pm/elixir/1.18.3/Kernel.html#then/2
- deleted 1y ago[deleted]
- AndyKluger 1y agoDo concatenative langs like Factor fit the bill?
- layer8 1y agoIt would be even better without the `>`, though. The `|>` is a bit awkward to type, and more noisy visually.
- MyOutfitIsVague 1y agoI disagree, because then it can be very ambiguous with an existing `|` operator. The language has to be able to tell that this is a pipeline and not doing a bitwise or operation on the output of multiple functions.
- layer8 1y agoYes, I’m talking about a language where `|` would be the pipe operator and nothing else, like in a shell. Retrofitting a new operator into an existing language tends to be suboptimal.
- hinkley 1y agoThe pipe operator relies on the first argument being the subject of the operation. A lot of languages have the arguments in a different order, and OO languages sometimes use function chaining to get a similar result.
- sparkie 1y agoYou could make use of `flip` from Haskell. flip :: (x -> y -> z) -> (y -> x -> x) flip f = \y -> \x -> f x y x |> (flip f)(y) -- f(x, y)
- Terr_ 1y agoIIRC the usual workaround in Elixir involves be small lambda that rearranges things: "World" |> then(&concat("Hello ", &1)) I imagine a shorter syntax could someday be possible, where some special placeholder expression could be used, ex: "World" |> concat("Hello ", &1) However that creates a new problem: If the implicit-first-argument form is still permitted (foo() instead of foo(&1)) then it becomes confusing which function-arity is being called. A human could easily fail to notice the absence or presence of the special placeholder on some lines, and invoke the wrong thing.
- hinkley 1y agoYeah I really hate that syntax and I can’t even explain why so I kind of blot it out, but you’re right. My dislike does improve my test coverage though, since I tend to pop out a real method instead.
- freehorse 1y agoYeah, R (tidyverse) has `.` as such a placeholder. It is useful but indeed I find the syntax off, though I find the syntax off even without it, anyway. I would rather define pipes as compositions of functions, which are pretty unambiguous in terms of what arguments they get, and then apply these to whatever i want.
- agent281 1y agoLast time I checked (2020) there were already a few rejected proposals to shorten the syntax for this. It seemed like they were pretty exasperated by them at the time.
- manmal 1y agoI wish there were a variation that can destructure more ergonomically. Instead of: ``` fetch_data() |> (fn {:ok, val, _meta} -> val :error -> "default value" end).() |> String.upcase() ``` Something like this: ``` fetch_data() |>? {:ok, val, _meta} -> val |>? :error -> "default value" |> String.upcase() ```
- smallerize 1y agofetch_data() |> case do {:ok, val, _meta} -> val :error -> "default value" end You have the extra "case do...end" block but it's pretty close? This is for sequential conditions. If you have nested conditions, check out a where block instead. https://dev.to/martinthenth/using-elixirs-with-statement-5e36#3-chained-functions-in-with-statements https://dev.to/martinthenth/using-elixirs-with-statement-5e3...
- manmal 1y agoThanks, that looks good!
- deleted 1y ago[deleted]
- bradford 1y agoI hate to be that guy, but I believe the `|>` syntax started with F# before Elixir picked it up. (No disagreements with your post, just want to give credit where it's due. I'm also a big fan of the syntax)
- ghthor 1y agoI turn older then f#, it’s been an ML language thing for a while but not sure where it first appeared
- ossopite 1y agoIt seems like it originated in the Isabelle proof assistant ML dialect in the mid 90s https://web.archive.org/web/20190217164203/https://blogs.msdn.microsoft.com/dsyme/2011/05/17/archeological-semiotics-the-birth-of-the-pipeline-symbol-1994/ https://web.archive.org/web/20190217164203/https://blogs.msd...
- matthewsinclair 1y agoAgree. This is absolutely my fave part of Elixir. Whenever I can get something to flow elegantly thru a pipeline like that, I feel like it’s a win against chaos.
- Alupis 1y agoPipelines are one of the greatest Gleam features[1]. [1] https://tour.gleam.run/functions/pipelines/ https://tour.gleam.run/functions/pipelines/
- dorian-graph 1y agoI wouldn't say it's a Gleam feature per se, in that it's not something that it's added that isn't already in Elixir.
- AdieuToLogic 1y ago> I would be lying if I didn't secretly wish that all languages adopted the `|>` syntax from Elixir. This is usually the Thrush combinator[0], exists in other languages as well, and can be informally defined as: f(g(x)) = g(x) |> f 0 - https://leanpub.com/combinators/read#leanpub-auto-the-thrush https://leanpub.com/combinators/read#leanpub-auto-the-thrush
- Munksgaard 1y agoNot quite. Note that the Elixir pipe puts the left hand of the pipe as the first argument in the right-hand function. E.g. x |> f(y) = f(x, y) As a result, the Elixir variant cannot be defined as a well-typed function, but must be a macro.
- deleted 1y ago[deleted]
- neonsunset 1y agoElixir itself adopted this operator from F#
- rkangel 1y agoI'm a big fan of the Elixir operator, and it should be standard in all functional programming languages. You need it because everything is just a function and you can't do anything like method chaining, because none of the return values have anything like methods. The |> is "just" syntax sugar for a load of nested functions. Whereas the Rust style method chaining doesn't need language support - it's more of a programming style. Note also that it works well in Elixir because it was created at the same time as most of the standard library. That means that the standard library takes the relevant argument in the first position all the time. Very rarely do you need to pipe into the second argument (and you need a lambda or convenience function to make that work).
- mvieira38 1y agoR has a lovely toolkit for data science using this syntax, called the tidyverse. My favorite dev experience, it's so easy to just write code
- AlchemistCamp 1y agoI've been using Elxir for a long time and had that same hope after having experienced how clear, concise and maintainable apps can be when the core is all a bunch of pipelines (and the boundary does error handling using cases and withs). But having seen the pipe operator in Ruby, I now think it was a bad idea. The problem is that method-chaining is common in several OO languages, including Ruby. This means the functions on an object return an object, which can then call other functions on itself. In contrast, the pipe operator calls a function, passing in what's on the left side of it as the first argument. In order to work properly, this means you'll need functions that take the data as the first argument and return the same shape to return, whether that's a list, a map, a string or a struct, etc. When you add a pipe operator to an OO language where method-chaining is common, you'll start getting two different types of APIs and it ends up messier than if you'd just stuck with chaining method calls. I much prefer passing immutable data into a pipeline of functions as Elixir does it, but I'd pick method chaining over a mix of method chaining and pipelines.