Comparing this proposal to the syntax from the better-known chaining and piping packages:
Example from Chain.jl Readme:
Proposed CCS+PAS | Base 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.jl | DataPipes.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)
|
Proposed CCS+PAS | Base 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.jl | DataPipes.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+PAS | Base 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.jl | DataPipes.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+PAS | Base 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.jl | DataPipes.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:
- Placeholder to specify argument position for partial function evaluation (“tight currying”)
- 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.