I've seen this a few times, maybe support could be added:
julia> map({:foo=>"foo", :bar=>"bar"}) do (k, v)
println("$k: $v")
end
ERROR: syntax: "(k,v)" is not a valid function argument name
Right now people fall back on:
map({:foo=>"foo", :bar=>"bar"}) do el
(k, v) = el
println("$k: $v")
end
+1
+1
Is this a general do block thing, or is it specific to map? Python has a starmap to do this, in Julia it would look a bit like:
starmap(fn, seq) = [ fn(i...) for i in seq ]
And solves the problem:
starmap({:foo=>"foo", :bar=>"bar"}) do k, v
println("$k: $v")
end
foo: foo
bar: bar
If do blocks had special support for unpacking tuples then it would be inconsistent and confusing when the user changed their code from using a do block to passing in a function and found that they now had to unpack their tuples manually again.
Obviously starmap
makes sense as a name in python but not in Julia, the literal translation would be ellipsismap
but I'm not sure that is a good name either.
splatmap
might be a better name.
Yes, it should be possible to support the syntax do (x, y)
, but it can't mean tuple splitting since function arguments don't mean that in other contexts.
There are enough other functions that apply a function argument to an iterator (pmap
, mapreduce
, any
, all
, count
, find
, sum
) that a general solution would be useful. One syntax that does not seem to be taken (it presently throws) is:
map(x) do splat(k, v) ... end
as sugar for
map(x) do x splat(x) do k, v ... end end
But maybe that's too confusing, because that syntax could also mean:
map(x) do
splat(k, v)
end
A macro (@splat map(x) do k, v ... end
) is another less invasive possibility, although it's also not immediately clear to someone reading the code what the macro does.
Another possibility is to automatically split the tuple with no new syntax, if the function is being passed a single tuple argument but the do block takes multiple arguments. That wouldn't be consistent for the case of a tuple of length 1, but I'm not sure that's a big deal. Not sure how this would be implemented, though.
I feel that implicitly splatting things in some cases would be the Path of Madness.
If this is useful enough that we are okay with extending the language for it, maybe just do... k, v
?
That's not a bad syntax. What's a little strange about introducing this is that it makes the do-block syntax(es) more flexible than the immediate lambda syntax, since there's no way to splat a single argument into multiple locals. Something like ...(k,v)->f(k,v)
could work, I suppose.
The real way to do this is to allow tuple expressions as formal arguments. Example:
f((x,y),) = x+y
would mean
function f(temp)
x,y = temp
x+y
end
Types would be specified as normal, e.g. f((x,y)::(Int,Int))
.
I guess if you want to be fancy f((x::Int, y::Int))
could work too.
This is basically converging with pattern matching at this point (which is a good thing IMO).
+1 for some variant of these ideas.
Tuple destructuring in function arguments is a feature I've already missed several times, and given that function arguments are essentially the left-hand side of an assignment it seems only consistent if this would work. Luckily, it is not too hard to write a macro for this:
using MacroTools
macro destructargs(expr)
# Disassemble function definition
if !@capture(expr,
begin
(f_(args__) = body_) |
(function f_(args__) body_ end) |
((args__,) -> body_) |
((args__) -> body_)
# In case of multiple arguments, (args__) -> body_ matches
# the argument tuple instead of the array of arguments.
# On the other hand, (args__,) -> body_ doesn't match a single-
# argument anonymous function. This is way we need two cases
# to cover anonymous functions.
end
)
error("@splatargs must be applied to a function definition")
end
if f == nothing
f = gensym()
end
# Check for tuple arguments and replace them with dummies
tupleargs = Pair{Symbol,Expr}[]
for i in eachindex(args)
if isa(args[i], Expr)
if args[i].head == :tuple
arg = gensym()
push!(tupleargs, arg => args[i])
args[i] = arg
elseif (
args[i].head == :(::) &&
isa(args[i].args[1],Expr) &&
args[i].args[1].head == :tuple
)
arg = gensym()
push!(tupleargs, arg => args[i].args[1])
args[i] = Expr(:(::),arg,args[i].args[2])
end
end
end
# Reassemble function definition
return Expr(:function,
esc(Expr(:call,f,args...)),
Expr(:block,
[:($(esc(tuple)) = $arg) for (arg,tuple) in tupleargs]...,
esc(body)
)
)
end
This essentially allows for the notation proposed by JeffBezanson:
julia> macroexpand(quote
@destructargs function foo((x,y)::Tuple{Int,Int})
x + y
end
end)
quote # none, line 2:
function foo(##7783::Tuple{Int,Int})
(x,y) = ##7783
x + y
end
end
I'm broadening this issue to "more general destructuring" and leaving it open for more design.
+1
Elixir has me spoiled!
It would be great to see pattern matching available on any kind of assignment.
+1
Please don't chime in on issues just to say "+1" – it's extremely annoying for people who read each and every single notification on this repo. This is why GitHub implemented reactions – if you must +1
, do it with a reaction.
Added by #23337
While this works "typically," it doesn't seem to work for do
blocks. This works:
julia> mktemp() do path, io
redirect_stdout(io) do
println("Hello, world!")
end
end
but just putting parentheses around the outputs of mktemp
leads to a fairly opaque error:
julia> mktemp() do (path, io)
redirect_stdout(io) do
println("Hello, world!")
end
end
ERROR: MethodError: no method matching (::getfield(Main, Symbol("##7#9")))(::String, ::IOStream)
Closest candidates are:
#7(::Any) at REPL[1]:2
Stacktrace:
[1] mktemp(::getfield(Main, Symbol("##7#9")), ::String) at ./file.jl:574
[2] mktemp(::Function) at ./file.jl:572
[3] top-level scope at none:0
I agree that the error message is super confusing, but that is actually the symptom of do (path, io)
being the syntax for the feature requested in this issue. So it would seem better to open an new issue about improving the error message, no?
My impression is that Jeff felt this had been implemented in #23337.
I don't see what isn't working, beyond the error being opaque.
Aha: mktemp
returns a tuple, and mktemp(::Function)
says it calls its argument function with the result of mktemp
. But that isn't true; it passes the values as two arguments instead.
Most helpful comment
Please don't chime in on issues just to say "+1" – it's extremely annoying for people who read each and every single notification on this repo. This is why GitHub implemented reactions – if you must
+1
, do it with a reaction.