Julia: allow tuple destructuring in formal arguments

Created on 23 Apr 2014  ·  25Comments  ·  Source: JuliaLang/julia

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

lowering

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.

All 25 comments

+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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

yurivish picture yurivish  ·  3Comments

omus picture omus  ·  3Comments

StefanKarpinski picture StefanKarpinski  ·  3Comments

StefanKarpinski picture StefanKarpinski  ·  3Comments

felixrehren picture felixrehren  ·  3Comments