Partially applied pipelines/currying/etc

176 views
Skip to first unread message

pragdave

unread,
Oct 25, 2022, 8:46:08 PM10/25/22
to elixir-lang-core

I know this has been discussed before, in different forms, but I thought I’d just have another go following my recent earth-shattering PR :)

I’d like to propose a new unary operator, &>. It works just like the pipeline |>, but doesn’t take a left argument. Instead it returns an arity 1 function that, when called, executes the pipeline.

toCamel = &> downcase |> split("_") |> map(&capitalize/1) |> join

toCamel.("now is")           # => "NowIs"
"the_time" |> toCamel.()     # => "the time"

Why?

  • Function composition is a key part of FP. It would be nice to support it natively.

  • The current pipeline semantic both defines a processing flow and executes it. Splitting this into two would allow a lot more expressiveness.

  • This would allow pipelines to be composed. Think Plug, but functional .

  pipeline :browser do     

  plug :accepts, ["html"] 

  plug :fetch_session     

  plug :fetch_flash     

  plug :protect_from_forgery
end

would become

browser_pipeline =
  &> accepts("html") |> fetch_session |> fetch_flash |> protect_from_forgery

The cool thing here is that each of the elements of the pipe is simply a function, and could be a nested pipeline. For example, we could create an authenticated pipeline by embedding the browser pipeline:

authenticated_pipeline = &> browser_pipeline.() |> authenticate

How?

If we created a new prefix operator in the interpreter, then we could implement a proof of concept as a library. Folks could then use this for a while to see if the idea has merit, and if so, possibly roll this into the language syntax.

I’d be happy to do this if there’s interest.

Dave

i Dorgan

unread,
Oct 25, 2022, 10:56:14 PM10/25/22
to elixir-l...@googlegroups.com
For the plugs example, how is it better than the alternative that exists today?

browser_pipeline =
  fn conn -> conn |> accepts("html") |> fetch_session() |> fetch_flash() |> protect_from_forgery() end

This would allow pipelines to be composed. Think Plug, but functional.
I think this is a preculiarity of how the plug/phoenix DSL works and not an issue with Elixir itself. Assuming there is a problem with the way pipelines are defined, wouldn't that call for a different design at the library level instead of a change in Elixir itself?

Function composition is a key part of FP.
Agreed, but functional composition(h = g ∘ f) is not the way the overwhelming majority of elixir code is written, as it's more "idiomatic" to write programs that operate on a data structure and not by composing functions together. We say that two functions compose when the input of one matches the input of the other and we fully leverage that to write pipelines, but we don't write programs by defining more functions via functional composition.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/a5b1186c-a4ea-4f50-8143-354738f47632n%40googlegroups.com.

Ben Wilson

unread,
Oct 26, 2022, 12:45:26 AM10/26/22
to elixir-lang-core
I think it would be helpful to see examples of regular Elixir code today that would be improved with this operator. The Plug example doesn't really work for me because Plug is doing a bunch of compile time stuff anyway and it also isn't using the pipe operator.

Pedro Carvalho

unread,
Oct 26, 2022, 1:35:04 AM10/26/22
to elixir-l...@googlegroups.com
For stuff like dbg that receives a function as a parameter it would be nice if one could do something like:

some_data
|> some_function
|> other_function
|>  &>  |> execute_piped_function()

It could also have a new operator that does the same job as  |>  &>  |>
maybe ||> ?

pragdave

unread,
Oct 26, 2022, 11:49:16 AM10/26/22
to elixir-lang-core
On Tuesday, October 25, 2022 at 9:56:14 PM UTC-5 dorga...@gmail.com wrote:
For the plugs example, how is it better than the alternative that exists today?

Because it is written without the magic. The way Plug currently works is needlessly nonfunctional and opaque.  With this small change, we could turn it from "some compile time magic" into "just more code.

Why write `Plug :accept, ["html"]` when you can just write `accept("html")` as part of a standard Elixir pipeline?


Function composition is a key part of FP.
Agreed, but functional composition(h = g ∘ f) is not the way the overwhelming majority of elixir code is written.

Of course, because Elixir doesn't support it. I'm suggesting a simple, non-breaking and noninvasice change that would add support. Then, in a year's time, we can see if there's any adoption.
 

Paul Schoenfelder

unread,
Oct 26, 2022, 12:57:39 PM10/26/22
to 'Justin Wood' via elixir-lang-core
A couple thoughts:

* The proposed operator `&>` may be ambiguous to parse due to function capture syntax, e.g. distinguishing between `&>` and `&>/2` might be problematic. A different operator would probably be better. José would have to chime in on that
* I like the idea of having a simple way of capturing a pipeline built in to the language, but I'm not really a fan of introducing more operators. My gut feeling is that I'd prefer a special form or plain macro instead, but if naming is an issue an operator might be the only option. Is there an equivalent in other languages? Nothing comes to mind as being a direct equivalent of this
* I'm not entirely convinced this is necessary when it is so trivial to wrap a pipeline expression in an anonymous function anyway, but maybe I'm just not working on stuff which benefits from this kind of functional composition where the improved ergonomics would be more noticeable.

Paul

Allen Madsen

unread,
Oct 26, 2022, 12:59:51 PM10/26/22
to elixir-l...@googlegroups.com
Plug isn't a great example, because it maps closer to a with than a pipeline, because any step can halt the pipeline. The compilation stuff removes the need for you to check if the pipeline is halted after each step. What you would write would be something roughly like:

with {:ok, conn} <- accept(conn, "html"),
  {:ok, conn} <- fetch_session(conn),
  ... do
  #...
else
  {:halt, conn} ->
    #...
end


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

José Valim

unread,
Oct 26, 2022, 1:53:01 PM10/26/22
to elixir-l...@googlegroups.com
Hi Dave!

I think Plug is a misleading example because Plug should actually be implemented with `with`:

with %Plug.Conn{halt: false} = conn <- plug1(conn, opts),
     %Plug.Conn{halt: false} = conn <- plug2(conn, opts),
     do: conn

That's because a Plug pipeline can abort at any moment. In this sense, we already have a construct that captures Plug needs. Other than that, Plug is compile-time exactly to provide compile-time initialization of (module) plugs to avoid runtime work.

Of course, one could argue that a pipeline should then be further decoupled into: functions, flow, and execution. This way, we could customize how a pipeline connects its functions, but at this point we are into monad territory. :)

The other thing is that the original code can be written as this:

& &1 |> downcase |> split("_") |> map(&capitalize/1) |> join

Which is only 5 characters longer. If the goal is to experiment and measure adoption, I would say we have a construct close enough for devs to explore, and then discuss streamlining it if it is common enough (like |> (expr).() became then/2).

Finally, I want to note that function composition may be expensive. Recent compilers optimizations certainly removed part of the cost, my gut feeling tells me the performance difference likely won't matter, but in terms of completeness, I would say it may be worth measuring in actual code.
--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Marc-André Lafortune

unread,
Oct 27, 2022, 3:56:20 PM10/27/22
to elixir-lang-core
On Wednesday, 26 October 2022 at 13:53:01 UTC-4 José Valim wrote:
The other thing is that the original code can be written as this:

& &1 |> downcase |> split("_") |> map(&capitalize/1) |> join

I had the exact same reflection.

BTW, this actually doesn't work, giving `nested captures are not allowed.` error, but that's not directly related (and seems like it could be resolved; second `&` isn't really a capture, right?).

José Valim

unread,
Oct 27, 2022, 4:31:44 PM10/27/22
to elixir-l...@googlegroups.com
We could resolve it, yes, and allow &capitalize/1 inside captures. I wouldn't oppose it.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Bruce Tate

unread,
Oct 27, 2022, 5:02:47 PM10/27/22
to elixir-l...@googlegroups.com
That's good news. Once resolved, the & &1 |> ... capture syntax would make a pretty good addition to Elixir. I know I would use it in my classes. 

-bt



--

Regards,
Bruce Tate
CEO

Marc-André Lafortune

unread,
Oct 28, 2022, 1:31:55 PM10/28/22
to elixir-l...@googlegroups.com
On Thu, Oct 27, 2022 at 4:31 PM José Valim <jose....@dashbit.co> wrote:
We could resolve it, yes, and allow &capitalize/1 inside captures. I wouldn't oppose it.

Cool. This sounds like fun, I'd like to take a stab at it

José Valim

unread,
Oct 28, 2022, 1:32:18 PM10/28/22
to elixir-l...@googlegroups.com
Please do!

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
Reply all
Reply to author
Forward
Message has been deleted
Message has been deleted
0 new messages