Fixing the Piping/Chaining/Partial Application Issue (Rev 2)

Comparing this proposal to the syntax from the better-known chaining and piping packages:


Example from Chain.jl Readme:

Proposed CCS+PASBase Julia
df--begin
  dropmissing
  filter(:id => >(6), _)
  groupby(_, :group)
  combine(_, :age => sum)
end
df |>
  dropmissing |>
  x -> filter(:id => >(6), x) |>
  x -> groupby(x, :group) |>
  x -> combine(x, :age => sum)
Chain.jlDataPipes.jl
@chain df begin
  dropmissing
  filter(:id => >(6), _)
  groupby(:group)
  combine(:age => sum)
end
@p begin
  df
  dropmissing
  filter(:id => >(6), x)
  groupby(_, :group)
  combine(_, :age => sum)
end
Pipe.jl Lazy.jl
@pipe df |>
  dropmissing |>
  filter(:id => >(6), _)|>
  groupby(_, :group) |>
  combine(_, :age => sum)
@> df begin
  dropmissing
  x -> filter(:id => >(6), x)
  groupby(:group)
  combine(:age => sum)
end
Underscores.jl Hose.jl
@_ df |>
  dropmissing |>
  filter(:id => >(6), __) |>
  groupby(__, :group) |>
  combine(__, :age => sum)
@hose df |>
  dropmissing |>
  filter(:id => >(6), _) |>
  groupby(_, :group) |>
  combine(_, :age => sum)

Example from DataPipes.jl Readme

Proposed CCS+PASBase Julia
"a=1 b=2 c=3"--begin
  split
  map(_) do --
    split(_, "=")
    Symbol(it[1]) => parse(Int, it[2])
  end
  NamedTuple
end
"a=1 b=2 c=3" |>
  split |>
  it->map(it) do it
    it=split(it, "=")
    Symbol(it[1]) => parse(Int, it[2])
  end |>
  NamedTuple
Chain.jlDataPipes.jl
@chain "a=1 b=2 c=3" begin
  split
  map(_) do it
    it=split(it, "=")
    Symbol(it[1]) => parse(Int, it[2])
  end
  NamedTuple
end
@p let
  "a=1 b=2 c=3"
  split
  map() do __  
    split(__, '=')
    Symbol(__[1]) => parse(Int, __[2])
  end
  NamedTuple
end
Pipe.jl Lazy.jl
@pipe "a=1 b=2 c=3" |>
  split |>
  map(_) do it
    it=split(it, "=")
    Symbol(it[1]) => parse(Int, it[2])
  end |>
  NamedTuple
@>> "a=1 b=2 c=3" begin
  split
  map(it->begin
    it=split(it, "=")
    Symbol(it[1]) => parse(Int, it[2])
  end)
  NamedTuple
end
Underscores.jl Hose.jl
@_ "a=1 b=2 c=3" |>
  split |>
  map(it->begin
    it=split(it, "=")
    Symbol(it[1]) => parse(Int, it[2])
  end, __) |>
  NamedTuple
@hose "a=1 b=2 c=3" |>
  split |>
  map(_) do it
    it=split(it, "=")
    Symbol(it[1]) => parse(Int, it[2])
  end |>
  NamedTuple

Examples from Pipe.jl Readme

Proposed CCS+PASBase Julia
a--b(_...)
a--b(it(1,2))
a--b(it[3])
(2,4)--get_angle(_,_)
a |> x->b(x...)
a |> x->b(x(1,2))
a |> x->b(x[3])
(2,4) |> x->get_angle(x[1],x[2])
Chain.jlDataPipes.jl
@chain a b(_...)
@chain a b(_(1, 2))
@chain a b(_[3])
@chain (2,4) get_angle(_[1],_[2])
@p a b(__...)
@p a b(__(1,2))
@p a b(__[3])
@p (2,4) get_angle(__[1], __[2])
Pipe.jl Lazy.jl
@pipe a |> b(_...)
@pipe a |> b(_(1, 2))
@pipe a |> b(_[3])
@pipe (2,4) |> get_angle(_[1],_[2])
# N/A
# N/A
# N/A
# N/A
Underscores.jl Hose.jl
@_ a |> b(__...)
@_ a |> b(__(1,2))
@_ a |> b(__[3])
@_ (2,4) |> get_angle(__[1],__[2])
@hose a |> b(_...)
@hose a |> b(_(1,2))
@hose a |> b(_[3])
@hose (2,4) |> get_angle(_[1],_[2])

My Examples

Proposed CCS+PASBase Julia
[1,2,3]--map(_^2, _)
[1,2,3]--join(_, ", ")
"1"--parse(Int, _) == 1
(:a,:b)--reverse--f(_...)
[1,2,3] |> x->map(x->x^2, x)
[1,2,3] |> x->join(x, ", ")
([1,2,3] |> x->parse(Int, x)) == 1
(:a,:b) |> reverse |> x->f(x...)
Chain.jlDataPipes.jl
@chain [1,2,3] map(x->x^2, _)
@chain [1,2,3] join(", ")
@chain("1", parse(Int, _)) == 1
@chain (:a,:b) reverse f(_...)
@p [1,2,3] map(_^2)
@p [1,2,3] join(__, ", ")
@p("1", parse(Int)) == 1
@p (:a,:b) reverse f(__...)
Pipe.jl Lazy.jl
@pipe [1,2,3] |> map(x->x^2, _)
@pipe [1,2,3] |> join(_, ", ")
@pipe("1" |> parse(Int, _)) == 1
@pipe (:a,:b) |> reverse |> f(_...)
@>> [1,2,3] map(x->x^2)
@> [1,2,3] join(", ")
@>>("1", parse(Int)) == 1
# N/A
Underscores.jl Hose.jl
@_ [1,2,3] |> map(_^2, __)
@_ [1,2,3] |> join(__, ", ")
@_("1" |> parse(Int, __)) == 1
@_ (:a,:b) |> reverse |> f(__...)
@hose [1,2,3] |> map(x->x^2, _)
@hose [1,2,3] |> join(_, ", ")
@hose("1" |> parse(Int, _)) == 1
@hose (:a,:b) |> reverse |> f(_...)

Of interest:

Many of these packages implement single-underscore _ and double-underscore __, each with a meaning, if not equal to, approximating:

  1. Placeholder to specify argument position for partial function evaluation (“tight currying”)
  2. Return value of last element in execution chain (“loose binding”)

It’s fairly confusing to identify which is which, because they seem to have similar meanings in these contexts, they look almost identical, and different packages swap their symbols or give them slightly different meanings.

In this proposal, to avoid confusing the two, I call #2 it reflecting the analogous role of the pronoun in the English language for method chaining, and I leave #1 as the most basic Scala-style argument placeholder _ for partial function evaluation.

My proposal intends to keep the behavior rules as simple and consistent as possible, to create a syntax that composes well, instead of fragile and complicated rules.


Some color on the word “it”

Method chaining is a common idiom in natural language too. In some instances, a sequence of methods is directly composable. For example (in pseudo-English):

Cat: Put on lap. Inspect fur. Find flea. Pull off. Put in soapy water.

In these instances, the pronoun “it” can be implied. In other instances, when methods are not directly composable and minor glue logic is necessary to ready an object for the next step in the chain, we must make “it” explicit:

Baby: Pick up. Lift its head above its legs. Put butt on your arm. Rock to sleep.

Notice that without the glue employing “it,” the composition might not work very well.

The call chain syntax I propose here intends to handle these cases, and uses the same keyword “it” for the exact same reasons.

Of course, there are more sophisticated scenarios where each object must be specified at each step:

Pot, oats, milk: Put the pot on the stove. Put oats in the pot. Pour milk in the pot. Turn on the stove.

For these more general (and more verbose) scenarios, we already have lambdas and named functions.