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

Stricter tuple destructuring. Error on (a, b) = (1, 2, 3) #37132

Open
goretkin opened this issue Aug 20, 2020 · 12 comments
Open

Stricter tuple destructuring. Error on (a, b) = (1, 2, 3) #37132

goretkin opened this issue Aug 20, 2020 · 12 comments
Labels
breaking This change will break code compiler:lowering Syntax lowering (compiler front end, 2nd stage)
Milestone

Comments

@goretkin
Copy link
Contributor

goretkin commented Aug 20, 2020

Current behavior

julia> (a, b) = (1, 2, 3) # or `a, b = (1, 2, 3)`
(1, 2, 3)

julia> a
1

julia> b
2

I think I'd like an error. This would be breaking on Julia 1.0, but @DilumAluthge suggested I open an issue to keep track of it possibly for Julia 2.0.

As mentioned in the manual, a Tuple can be thought of a positional-argument-only function call without the function. A function call binds values in the calling context to the formal arguments in the function body context, and so it could be nice to have tuple assignment behave more similarly to that binding process. In other words, you cannot call f(a, b) as f(1, 2, 3).

In other words still, if tuple destructuring were more strict, then the two functions g and h would be equivalent (up to error type):

function g(args)
  function f(a, b)
    a * b
  end

  f(args...)
end

function h(args)
  let (a, b) = args
    a * b
  end
end
julia> h((2,3))
6

julia> g((2,3))
6

julia> g((2,3,4))
ERROR: MethodError: no method matching (::var"#f#59")(::Int64, ::Int64, ::Int64)
Closest candidates are:
  f(::Any, ::Any)

julia> h((2,3,4))
6

h((2,3,4)) would throw some kind of error (not MethodError).

This is what happens today:

julia> Meta.@lower (a, b) = c
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─ %1 = Base.indexed_iterate(c, 1)
│   %2 = Core.getfield(%1, 1)
│        a = %2
│        #s332 = Core.getfield(%1, 2)
│   %5 = Base.indexed_iterate(c, 2, #s332)
│   %6 = Core.getfield(%5, 1)
│        b = %6
└──      return c
))))

If the strict behavior is desired, I don't know whether to enforce it with a length call, or whether to more generally check that the iterator is empty at the end.

@goretkin
Copy link
Contributor Author

Related
#32547 (comment)
#2626

@StefanKarpinski StefanKarpinski added this to the 2.0 milestone Aug 20, 2020
@StefanKarpinski StefanKarpinski added breaking This change will break code compiler:lowering Syntax lowering (compiler front end, 2nd stage) labels Aug 20, 2020
@StefanKarpinski
Copy link
Member

I agree that this was probably a misfeature.

@thofma
Copy link
Contributor

thofma commented Aug 20, 2020

If this is changed, should the same rule also apply to arrays (or anything supporting iterate) on the right hand side? Would be nice if it were consistent. Since not everything supporting the Iterator interface has a length that one could query for, this probably means checking if the iterator is empty.

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Aug 20, 2020

That does seem to be what's implied. So you iterate to destructure, then assert that the iterator is empty. The LHS syntax a, b, ... = could just be a way of suppressing the assertion that the iterator is empty, i.e. to ask for the current behavior where the rest is just ignored.

@goretkin
Copy link
Contributor Author

We could make it so that this at least parses in 1.x, right?

julia> :((a,b, ...) = c)
ERROR: syntax: invalid identifier name "..."

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Aug 21, 2020

If it parses then giving it a new meaning is breaking, so we should not do that unless we give it the meaning that we ultimately want it to have. If the idea is to allow a, b, ... = RHS now so that people can start writing that as a future-proof way to explicitly indicate that they want to discard the rest of the RHS, then yes, that could make sense, but we'd have to come to an agreement that we want to do this and about the syntax, which I don't think we're at yet.

@JeffBezanson
Copy link
Member

The current behavior makes it possible to add extra output arguments to a function in the future without breaking callers. I don't see the value of forcing callers to acknowledge extra elements they are not interested in.

@StefanKarpinski
Copy link
Member

The trouble is that adding extra return values still isn't non-braking because people might do things like apply length to the returned tuple.

@JeffBezanson
Copy link
Member

Yes, but this change would strictly increase the potential for breakage.

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Aug 25, 2020

True, but since something is either breaking or not I'm not sure that helps that much. Would we generally add return values from Base functions?

@goretkin
Copy link
Contributor Author

goretkin commented Aug 25, 2020

The current behavior makes it possible to add extra output arguments to a function in the future without breaking callers. I don't see the value of forcing callers to acknowledge extra elements they are not interested in.

I could be off base, but it seems reasonable to think of this feature as the return-counterpart of optional positional arguments. if I have a function

f(a, b) = a + b

I can later define

f(a, b, c=1) = c * (a + b)

and it will be a non-breaking change.

(It's not totally correct, since multiple return values are "mechanically" the counterpart to argument destructuring

julia> h((a,b)) = a + b
h (generic function with 1 method)

julia> h((1,2))
3

julia> h((1, 2, 3))
3

)

I don't know what conclusion that takes us to exactly, but it roughly appears that there is a conflict for what Tuple models. I don't know what I mean by "fundamental", or "model" , but it feels appropriate to believe that tuples can either be fundamentally a way to support multiple return values (and support "optional return values"), or they can fundamentally model [non-optional] positional method arguments. Note that optional positional functional arguments are handled by defining extra methods, so it's technically correct to say tuples model positional method arguments without the caveat about [non-optional].

Does this feature in Julia have anything to do with varargout in MATLAB? Obviously Julia functions cannot tell whether you invoked them as (a, b) = f() or (a, b, c) = f(), but this feature does allow f to support both invocations. Presumably computing c should be avoided in the first invocation, which would only be possible if the f call were inlined. I would personally never use this feature, however. And I would be pretty confused if a function (in Base or anywhere) thought it was a non-breaking change to return extra elements in a tuple. I think it's telling that https://docs.julialang.org/en/v1/manual/functions/#Multiple-Return-Values-1 doesn't mention anything about optional return values.

For this "optional return values" feature to be safe to use, functions could return e.g. ReturnValues, which can't allow length(rv), can't allow splatting ([rv...]), cannot allow iteration (collect(rv)), and throws a sort of BoundsError which cannot be caught (otherwise you could implement length by probing getindex). In short, you can only do e.g. (a, b) = rv and rv[1]. Finally, it's Bad TM that this "extra return values" feature (whether implemented with Tuple or with ReturnValues) doesn't help you probably where you're most likely to want it, which is when functions currently return one argument, and you want them to return more (since presumably a function that returns one argument doesn't return a one-element tuple).

Finally, it seems like these features should compose in some way. I usually think of functions composing like f(g(x)), but for these features to compose, it seems like you'd need to think of functions composing like f(g(x...)...).

In summary:

Optional Positional Argument:

  • allow trailing unused arguments
  • implemented with multiple methods
  • non-breaking to add extra argument to a one-argument function
  • a function can tell whether the caller provided an optional argument (since there are different methods)

Optional Return Value:

  • allow trailing unused return values
  • implemented with non-strict tuple destructuring
  • breaking to add an extra return value to a one-return-value function
  • a function cannot tell whether the caller use an optional return value.

@adienes
Copy link
Contributor

adienes commented Jul 26, 2024

a reasonable candidate for #54903

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

No branches or pull requests

5 participants