In most of the language x, y and (x, y) behave identically in creating a tuple. In the arguments to a do-function, only (x, y) destructures a tuple while x, y creates two arguments:
julia> methods() do (x, y) end
# 1 method for anonymous function "#13":
[1] (::var"#13#14")(::Any) in Main at REPL[10]:1
julia> methods() do x, y end
# 1 method for anonymous function "#15":
[1] (::var"#15#16")(x, y) in Main at REPL[11]:1
I understand that this is consistent with how things work in the normal "function argument scope", but it's different than how things work in anonymous "function argument scope", which it seems like do-blocks should behave more similarly to:
julia> f((x, y)) = nothing;
julia> methods(f)
# 1 method for generic function "f":
[1] f(::Any) in Main at REPL[3]:1
julia> methods(function((x, y)) nothing end)
# 1 method for anonymous function "#5":
[1] (::var"#5#6")(x, y) in Main at REPL[5]:1
Mostly I wanted to bring this up as a discussion for julia 2.0 because it just doesn't feel right that do x, y and do (x, y) do completely separate things.
It also seems confusing that normal "function argument scope" and anonymous "function argument scope" behave differently in this way. Should I open a separate issue for that?
There are other places where presence or absence of parens matters, e.g.:
f(a, b) = ... # method that takes two arguments
f((a, b)) = ... # method that takes one argument and destructures it into `a` and `b`
That's very similar to the way destructuring works in do blocks. It is a pretty easy one to mess up鈥擨've done it myself. On the other hand, there are cases where it's really nice to have a clean syntax for destructuring a single argument, which otherwise is kind of nasty to write. Here's an example:
julia> d = Dict("one" => 1, "two" => 2, "three" => 3)
Dict{String,Int64} with 3 entries:
"two" => 2
"one" => 1
"three" => 3
julia> filter(d) do (k, v)
isodd(v)
end
Dict{String,Int64} with 2 entries:
"one" => 1
"three" => 3
julia> filter(((k, v),) -> isodd(v), d)
Dict{String,Int64} with 2 entries:
"one" => 1
"three" => 3
That second syntax is just nasty.
Related: https://github.com/JuliaLang/julia/issues/6484, about destructuring in for loops.
There are other places where presence or absence of parens matters, e.g.:
f(a, b) = ... # method that takes two arguments f((a, b)) = ... # method that takes one argument and destructures it into `a` and `b`
I noted this one in the OP. It's still surprising because anonymous functions don't have this distinction: function (x, y) end and function ((x, y)) end are equivalent.
Maybe a solution is to require a comma after a destructuring tuple similar to how it works with anonymous functions? do (x, y) and do x, y would be equivalent with no destructuring, but do (x, y), would destructure? The absence of a closing paren makes this awkward and possibly unworkable though (makes white-space significant).
I think how
function f(a, b) end
function f((a, b)) end
function (a, b) end
function ((a, b)) end
work is natural once you accept how
tuple(a, b)
tuple((a, b))
(a, b)
((a, b))
work.
I do think that the fact that (a, b) -> a + b and function ((a, b)); a + b; end produce two-argument functions is arguably a bit iffy. If anything, I would consider changing that parsing, but it doesn't seem pressing.
Most helpful comment
There are other places where presence or absence of parens matters, e.g.:
That's very similar to the way destructuring works in do blocks. It is a pretty easy one to mess up鈥擨've done it myself. On the other hand, there are cases where it's really nice to have a clean syntax for destructuring a single argument, which otherwise is kind of nasty to write. Here's an example:
That second syntax is just nasty.