Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow tuple destructuring in formal arguments #6614

Closed
WestleyArgentum opened this issue Apr 23, 2014 · 25 comments
Closed

allow tuple destructuring in formal arguments #6614

WestleyArgentum opened this issue Apr 23, 2014 · 25 comments
Assignees
Labels
compiler:lowering Syntax lowering (compiler front end, 2nd stage)
Milestone

Comments

@WestleyArgentum
Copy link
Member

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

@simonster
Copy link
Member

+1

@kmsquire
Copy link
Member

+1

@burrowsa
Copy link
Contributor

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.

@StefanKarpinski
Copy link
Sponsor Member

splatmap might be a better name.

@JeffBezanson
Copy link
Sponsor Member

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.

@simonster
Copy link
Member

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.

@simonster
Copy link
Member

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.

@JeffBezanson
Copy link
Sponsor Member

I feel that implicitly splatting things in some cases would be the Path of Madness.

@simonster
Copy link
Member

If this is useful enough that we are okay with extending the language for it, maybe just do... k, v?

@StefanKarpinski
Copy link
Sponsor Member

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.

@JeffBezanson
Copy link
Sponsor Member

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

@JeffBezanson
Copy link
Sponsor Member

I guess if you want to be fancy f((x::Int, y::Int)) could work too.

@StefanKarpinski
Copy link
Sponsor Member

This is basically converging with pattern matching at this point (which is a good thing IMO).

@JeffBezanson JeffBezanson changed the title extend do block syntax to support splitting tuple args allow tuple destructuring in formal arguments Apr 29, 2014
@sebastiang
Copy link

+1 for some variant of these ideas.

@ettersi
Copy link
Contributor

ettersi commented Aug 5, 2016

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

@StefanKarpinski StefanKarpinski added this to the 1.0 milestone Aug 25, 2016
@StefanKarpinski
Copy link
Sponsor Member

I'm broadening this issue to "more general destructuring" and leaving it open for more design.

@bitmage
Copy link

bitmage commented Sep 23, 2016

+1

Elixir has me spoiled!

It would be great to see pattern matching available on any kind of assignment.

@JeffBezanson JeffBezanson added the compiler:lowering Syntax lowering (compiler front end, 2nd stage) label Apr 7, 2017
@JeffBezanson JeffBezanson self-assigned this Apr 7, 2017
@JeffBezanson JeffBezanson modified the milestones: 2.0+, 1.0 May 2, 2017
@dovahcrow
Copy link
Contributor

+1

@StefanKarpinski
Copy link
Sponsor Member

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.

@JeffBezanson
Copy link
Sponsor Member

Added by #23337

JeffBezanson added a commit that referenced this issue Aug 31, 2017
RFC: implement #6614, destructuring in formal arguments
@timholy
Copy link
Sponsor Member

timholy commented Aug 1, 2018

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

@timholy timholy reopened this Aug 1, 2018
@StefanKarpinski
Copy link
Sponsor Member

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?

@StefanKarpinski StefanKarpinski modified the milestones: 1.x, 0.7 Aug 1, 2018
@timholy
Copy link
Sponsor Member

timholy commented Aug 1, 2018

My impression is that Jeff felt this had been implemented in #23337.

@JeffBezanson
Copy link
Sponsor Member

I don't see what isn't working, beyond the error being opaque.

@JeffBezanson
Copy link
Sponsor Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler:lowering Syntax lowering (compiler front end, 2nd stage)
Projects
None yet
Development

No branches or pull requests