Multiple assignment in conditional

I find this a strange Ruby error.

foo = [1,2]

# The following is fine.
if (a = foo)
  puts a
end

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)
  puts a
end

Why would that be a syntax error? Surely the grammar is

  if EXPR
    ...

and (a, b = foo) is an expression, isn't it?

Just curious, though my curiosity was prompted by a failing piece of real
code.

Gavin

What did you expect your real code to do? It seems to me that it should
raise an error unless it's an array of size 2, and if it is, then it should
return an array of size two, making the conditional necessarily true. So I
don't know what you were expecting to happen.

···

On Tue, Jan 31, 2012 at 12:46 AM, Gavin Sinclair <gsinclair@gmail.com>wrote:

I find this a strange Ruby error.

foo = [1,2]

# The following is fine.
if (a = foo)
puts a
end

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)
puts a
end

Why would that be a syntax error? Surely the grammar is

if EXPR
   ...

and (a, b = foo) is an expression, isn't it?

Just curious, though my curiosity was prompted by a failing piece of real
code.

Gavin

Hi Gavin,

I find this a strange Ruby error.

foo = [1,2]

# The following is fine.
if (a = foo)
   puts a
end

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)
   puts a
end

Why would that be a syntax error? Surely the grammar is

   if EXPR
     ...

and (a, b = foo) is an expression, isn't it?

Just curious, though my curiosity was prompted by a failing piece of real
code.

I can understand why the language rejects that construct (I've never tried it myself- an interesting find!). The method itself would be returning an array (correct?), and an array would always be treated as true in the expression, regardless of the results. Thus, it is likely to be some sort of error (why put an always-statically-true expression in a conditional?), and hence knocks it back with an error.

I'm wondering as to the best behaviour if it were actually handled directly by the language (which I'm not specifically suggesting). True if any of the multiple assignments evaluate to true? Or just the first? Or last? Certainly not always-true. I can't say I'd know a sensible default. :confused:

Garth

···

On 31/01/12 17:16, Gavin Sinclair wrote:

because it's a parallel assignment and ruby would not know which to
check, a or b, no?

try eg,

  a,b=true
or
  a,b,c=1

kind regards -botp

···

On Tue, Jan 31, 2012 at 2:46 PM, Gavin Sinclair <gsinclair@gmail.com> wrote:

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)
puts a
end

I find this a strange Ruby error.

foo = [1,2]

# The following is fine.
if (a = foo)
puts a
end

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)
puts a
end

Why would that be a syntax error? Surely the grammar is

if EXPR
...

and (a, b = foo) is an expression, isn't it?

Yeah, I'd agree. But why write such obfuscated code?

Just curious, though my curiosity was prompted by a failing piece of real
code.

Well, there is no point in having assignments in conditionals - unless
it's a loop in which case the code often becomes more elegant, e.g.:

while ( line = gets )
  puts line
end

For a simple if and unless there is really no advantage in having an
assignment in the condition.

Kind regards

robert

···

On Tue, Jan 31, 2012 at 7:46 AM, Gavin Sinclair <gsinclair@gmail.com> wrote:

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)

I can understand why the language rejects that construct (I've never tried
it myself- an interesting find!). The method itself would be returning an
array (correct?), and an array would always be treated as true in the
expression, regardless of the results. Thus, it is likely to be some sort of
error (why put an always-statically-true expression in a conditional?), and
hence knocks it back with an error.

if true
  puts 'well that’s a relief'
end

I'm wondering as to the best behaviour if it were actually handled directly
by the language (which I'm not specifically suggesting). True if any of the
multiple assignments evaluate to true? Or just the first? Or last? Certainly
not always-true. I can't say I'd know a sensible default. :confused:

[a, b, …].any?

···

On Tue, Jan 31, 2012 at 08:57, Garthy D <garthy_lmkltybr@entropicsoftware.com> wrote:

On 31/01/12 17:16, Gavin Sinclair wrote:

ok, ignore. i get gavin's point regarding expression

···

On Tue, Jan 31, 2012 at 8:38 PM, botp <botpena@gmail.com> wrote:

try eg,

a,b=true
or
a,b,c=1

i dont know, Robert, since i also find

  while ( line,x,y = get_something)

also elegant.
if ruby would support that, wouldn't it be elegant?

best regards -botp

···

On Wed, Feb 1, 2012 at 6:16 AM, Robert Klemme <shortcutter@googlemail.com> wrote:

it's a loop in which case the code often becomes more elegant, e.g.:

while ( line = gets )

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)
puts a
end

What did you expect your real code to do? It seems to me that it should
raise an error unless it's an array of size 2

Nope. It can return anything at all and Ruby will deal with it.
Parallel assignment is (thankfully) flexible.

def foo; 1; end
a, b = foo # returns 1; a == 1, b == nil

def bar; [1,2,3,4,5]; end
a, b = bar # returns [1,2,3,4,5]; a == 1, b == 2

and if it is, then it should
return an array of size two, making the conditional necessarily true.

Nope. The conditional can be false.

def foo; nil; end
a, b = foo # returns nil; a == nil, b == nil

So I don't know what you were expecting to happen.

def foo; nil; end

if (a, b = foo)
  puts "outcome 1"
else
  puts "outcome 2" # I was expecting this.
end

Of course I don't have a function that returns nil unconditionally!
Just showing the structure.

···

On Tue, Jan 31, 2012 at 6:22 PM, Josh Cheek <josh.cheek@gmail.com> wrote:

Here's the code with the thing that doesn't work. Of course you don't
know the context, but I hope this is enlightening.

  # week_and_day(date) -> [17,1] week 17, day 1 (Mon)
  # Returns nil if the given date is not in this term.
  def week_and_day(date)
    if not self.include? date
      return nil
    elsif (week, day = @t1.week_and_day(date))
      return [week, day]
    elsif (week, day = @t2.week_and_day(date))
      week += @t1.number_of_weeks
      return [week, day]
    end
  end

I don't see anything obfuscated about that. @t1.week_and_day(date)
will return nil if the date is outside the range of @t1, so we will go
to the next part of the conditional.

Here it is in acceptable Ruby.

  # week_and_day(date) -> [17,1] week 17, day 1 (Mon)
  # Returns nil if the given date is not in this term.
  def week_and_day(date)
    if not self.include? date
      return nil
    elsif (week_and_day = @t1.week_and_day(date))
      return week_and_day
    elsif (week_and_day = @t2.week_and_day(date))
      week, day = week_and_day
      week += @t1.number_of_weeks
      return [week, day]
    end
  end

This one has a clumsy variable name "week_and_day" and an extra line
to unpack it into "week" and "day". I'm not crying mercy over one
extra line, but I definitely prefer the first version. It seems more
intentional.

Gavin

···

On Wed, Feb 1, 2012 at 9:16 AM, Robert Klemme <shortcutter@googlemail.com> wrote:

Yeah, I'd agree. But why write such obfuscated code?

Hi Nikolai,

# The following causes a SyntaxError: multiple assignment in conditional.
if (a, b = foo)

I can understand why the language rejects that construct (I've never tried
it myself- an interesting find!). The method itself would be returning an
array (correct?), and an array would always be treated as true in the
expression, regardless of the results. Thus, it is likely to be some sort of
error (why put an always-statically-true expression in a conditional?), and
hence knocks it back with an error.

if true
   puts 'well that’s a relief'
end

Haha, I *knew* someone would call me out on that one if I wasn't more specific. What I didn't expect was how little time it'd take. :wink:

To clarify: If "if (a, b = foo)" would always evaluate to true, it might cause confusion for someone expecting it to base the evaluation on one of the assigned variables; whereas the intent for "if true" is pretty clear; it's an expression with less than two assignments.

Still, it's not necessarily a strong argument or one I'd get behind. I'm just saying that I understand why it might have turned out that way. Maybe the potential for confusion led to that particular design decision?

I'm wondering as to the best behaviour if it were actually handled directly
by the language (which I'm not specifically suggesting). True if any of the
multiple assignments evaluate to true? Or just the first? Or last? Certainly
not always-true. I can't say I'd know a sensible default. :confused:

[a, b, …].any?

Indeed, that's another possibility, although I'll point out that it's inconsistent with an empty array normally being treated as true in an ordinary expression. But hey, we are working with an odd case here, so why not? :slight_smile:

Garth

···

On 31/01/12 18:57, Nikolai Weibull wrote:

On Tue, Jan 31, 2012 at 08:57, Garthy D > <garthy_lmkltybr@entropicsoftware.com> wrote:

On 31/01/12 17:16, Gavin Sinclair wrote:

I don't think it would be elegant, because the relationship between the
condition and the assignment is obscure. I would assume it's a shorthand
for `loop { line, x, y = get_something; ... }` but would have to look at it
for a while and then run some experiments to see.

···

On Tue, Jan 31, 2012 at 6:22 PM, botp <botpena@gmail.com> wrote:

On Wed, Feb 1, 2012 at 6:16 AM, Robert Klemme > <shortcutter@googlemail.com> wrote:
> it's a loop in which case the code often becomes more elegant, e.g.:
>
> while ( line = gets )

i dont know, Robert, since i also find

while ( line,x,y = get_something)

also elegant.
if ruby would support that, wouldn't it be elegant?

best regards -botp

I find

something.each do |line, x, y|
  ...
end

more elegant and idiomatic Ruby. That's why I typically use

ARGF.each do |line|
  ...
end

instead of while ( line = gets ).

Kind regards

robert

···

On Wed, Feb 1, 2012 at 1:22 AM, botp <botpena@gmail.com> wrote:

On Wed, Feb 1, 2012 at 6:16 AM, Robert Klemme > <shortcutter@googlemail.com> wrote:

it's a loop in which case the code often becomes more elegant, e.g.:

while ( line = gets )

i dont know, Robert, since i also find

while ( line,x,y = get_something)

also elegant.
if ruby would support that, wouldn't it be elegant?

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

Sorry to reply to my own message, but I thought of something I should
have included.

Here is the forbidden version I would like to write. Obviously I'm
combining assignment and conditional, which may not win plaudits as a
general rule.

def week_and_day(date)
if not self.include? date
return nil
elsif (week, day = @t1.week_and_day(date))
return [week, day]
elsif (week, day = @t2.week_and_day(date))
week += @t1.number_of_weeks
return [week, day]
end
end

But consider this alternative, where I separate the assignment and the
conditional.

  def week_and_day(date)
    if not self.include? date
      return nil
    else
      week, day = @t1.week_and_day(date)
      if week
        return [week, day]
      else
        week, day = @t2.week_and_day(date)
        week += @t1.number_of_weeks
        return [week, day]
      end
    end
  end

I consider this very clumsy. By _not_ combining assignment with
conditional, I cannot use "elsif". I must split the "else" and the
"if", and turn the second conditional into a child instead of a
sibling.

Definitely less intentional and unattractive code.

Gavin

···

On Wed, Feb 1, 2012 at 10:57 PM, Gavin Sinclair <gsinclair@gmail.com> wrote:

You can trick ruby:

def foo; nil; end

if (_ = (a, b = foo))
   puts "outcome 1"
else
   puts "outcome 2"
end

So, the question is parsing, not semantics.

···

On 02/01/2012 03:49 AM, Gavin Sinclair wrote:

def foo; nil; end

if (a, b = foo)
   puts "outcome 1"
else
   puts "outcome 2" # I was expecting this.
end

Sorry, but I find this incredibly opaque.

Could you just write what you expect the evaluation to be in each of these
cases?

a, b = 1, 1
a, b = 1, nil
a, b = nil, 1
a, b = nil, nil

I'd expect return values of [1, 1], [1, nil], [nil, 1], [nil, nil] which
are all true.

Now does this change when the RHS is an array instead of two values?
For example, would `a, b = [1, 1]` return a different value than `a, b =
[1, 1]`?

Your example seems particularly devious in that it only has one value on
the RHS of the assignment. This means that the second argument will be
assigned nil (because there is no value for it), and because assignment
operators always return the RHS, it will then return whatever single value
was over there. This causes it to look like you are assigning multiple
values to the variables and having it return nil, when in fact you are only
assigning one value (the second variable is along for the ride and will
always be nil).

There are so many nuanced ruby edge cases one must know (and then must also
know the implementation of how it is being returned) to understand that
example that I think it has more overhead than it is worth.

···

On Wed, Feb 1, 2012 at 5:49 AM, Gavin Sinclair <gsinclair@gmail.com> wrote:

On Tue, Jan 31, 2012 at 6:22 PM, Josh Cheek <josh.cheek@gmail.com> wrote:

>> # The following causes a SyntaxError: multiple assignment in
conditional.
>> if (a, b = foo)
>> puts a
>> end

> What did you expect your real code to do? It seems to me that it should
> raise an error unless it's an array of size 2

Nope. It can return anything at all and Ruby will deal with it.
Parallel assignment is (thankfully) flexible.

def foo; 1; end
a, b = foo # returns 1; a == 1, b == nil

def bar; [1,2,3,4,5]; end
a, b = bar # returns [1,2,3,4,5]; a == 1, b == 2

> and if it is, then it should
> return an array of size two, making the conditional necessarily true.

Nope. The conditional can be false.

def foo; nil; end
a, b = foo # returns nil; a == nil, b == nil

> So I don't know what you were expecting to happen.

def foo; nil; end

if (a, b = foo)
  puts "outcome 1"
else
puts "outcome 2" # I was expecting this.
end

Of course I don't have a function that returns nil unconditionally!
Just showing the structure.

There are plenty more solutions, for example:

def week_and_day(date)
   return nil unless include? date

   week_and_day = @t1.week_and_day(date) and return week_and_day

   week_and_day = @t2.week_and_day(date)
   week_and_day[0] += @t1.number_of_weeks if week_and_day

   week_and_day
end

Maybe also

def week_and_day(date)
   return nil unless include? date

   case
   when week_and_day = @t1.week_and_day(date)
     nil
   when week_and_day = @t2.week_and_day(date)
     week_and_day[0] += @t1.number_of_weeks
   end

   week_and_day
end

Kind regards

robert

···

On Wed, Feb 1, 2012 at 1:05 PM, Gavin Sinclair <gsinclair@gmail.com> wrote:

On Wed, Feb 1, 2012 at 10:57 PM, Gavin Sinclair <gsinclair@gmail.com> wrote:

Sorry to reply to my own message, but I thought of something I should
have included.

Here is the forbidden version I would like to write. Obviously I'm
combining assignment and conditional, which may not win plaudits as a
general rule.

def week_and_day(date)
if not self.include? date
return nil
elsif (week, day = @t1.week_and_day(date))
return [week, day]
elsif (week, day = @t2.week_and_day(date))
week += @t1.number_of_weeks
return [week, day]
end
end

But consider this alternative, where I separate the assignment and the
conditional.

def week_and_day(date)
if not self.include? date
return nil
else
week, day = @t1.week_and_day(date)
if week
return [week, day]
else
week, day = @t2.week_and_day(date)
week += @t1.number_of_weeks
return [week, day]
end
end
end

I consider this very clumsy. By _not_ combining assignment with
conditional, I cannot use "elsif". I must split the "else" and the
"if", and turn the second conditional into a child instead of a
sibling.

Definitely less intentional and unattractive code.

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

You can trick ruby:

def foo; nil; end

if (_ = (a, b = foo))

puts "outcome 1"
else
puts "outcome 2"
end

So, the question is parsing, not semantics.

indeed. ruby just checks the last assignment.

if (x,y=(a, b = foo))
p 1
end

SyntaxError: (irb):17: multiple assignment in conditional

but i do not know why it has to do that checking. too much work. and
could probably slow down ruby.

kind regards -botp

···

On Thu, Feb 2, 2012 at 4:15 AM, Joel VanderWerf <joelvanderwerf@gmail.com> wrote:

Could you just write what you expect the evaluation to be in each of these
cases?

a, b = 1, 1
a, b = 1, nil
a, b = nil, 1
a, b = nil, nil

I'd expect return values of [1, 1], [1, nil], [nil, 1], [nil, nil] which
are all true.

Yes, I expect what you expect.

Also,

  a, b = nil # a and b both get the value nil

Now does this change when the RHS is an array instead of two values?
For example, would `a, b = [1, 1]` return a different value than `a, b =
[1, 1]`?

No, because you wrote them exactly the same!

Assuming you meant to compare these two:

  a, b = 1, 1
  a, b = [1, 1]

then I expect them to behave exactly the same. I can see why you
expect it to set a to [1,1] and b to nil, but that's just not the way
it is. If I'm not wrong, Ruby actually converts the first line above
into the second line behind the scenes. After all, that's precisely
how you return multiple values from a method, or at least give the
appearance of doing so.

  def two_values(x)
    [x*2, x^2]
  end

  a, b = two_values(5)

From faint memory, to achieve the same thing in Perl you must wrap the
LHS in an array. Ruby's equivalent would be

  [a, b] = two_values(5) # This doesn't work in Ruby, and it's not missed.

Your example seems particularly devious in that it only has one value on
the RHS of the assignment.

No it doesn't. That's not a value, per se, it's a method call. The
method call returns either an array, which assigns the two variables
and activates the "if" branch, or nil, which activates the "else"
branch.

I didn't approach this being devious or thinking it would be at all
controversial. I simply have a good intuition for assignment in Ruby.

This means that the second argument will be
assigned nil (because there is no value for it),

No. Try this in irb:

  if _ = (a, b = [5,2,16,100])
    puts a + b
  end

And this.

  a, b = nil
  a, b = 1
  a, b = 1, 2
  a, b = [1, 2]
  a, b = [1,2,3,4,5]

  a, *b = nil
  a, *b = 1
  a, *b = 1, 2
  a, *b = [1, 2]
  a, *b = [1,2,3,4,5]

and see what these assignment operators return and what values get set
to a and b.

[...]

There are so many nuanced ruby edge cases one must know (and then must also
know the implementation of how it is being returned) to understand that
example that I think it has more overhead than it is worth.

There isn't a single nuance [1]; it all follows from understanding
parallel assignment in Ruby. I didn't go out of my way to learn that.
It must have been in one of the early Ruby books I read, or
something, and something I use all the time (especially multiple
returns from methods).

[1] in 1.9.3. The earlier versions have the nuance of

  a, b = nil # Returns [nil] instead of nil.

That pre-1.9.3 behaviour is contrary to what I've always expected and
I'm glad to see it corrected. I can only surmise that the syntax
error "multiple assignment in conditional" is a hangover from those
days as a (fair) warning that it's not going to do what you expect,
and no good can come of it. They've changed the behaviour, but left
the completely unnecessary syntax error.

Gavin

···

On Thu, Feb 2, 2012 at 8:03 AM, Josh Cheek <josh.cheek@gmail.com> wrote:

Done and done!

  def week_and_day(date)
    if not self.include? date
      nil
    elsif _ = (week, day = @t1.week_and_day(date))
      [week, day]
    elsif _= (week, day = @t2.week_and_day(date))
      [week + @t1.number_of_weeks, day]
    end
  end

I guess I need to document a Ruby 1.9.3+ requirement :slight_smile:

···

On Thu, Feb 2, 2012 at 7:15 AM, Joel VanderWerf <joelvanderwerf@gmail.com> wrote:

You can trick ruby:

def foo; nil; end

if (_ = (a, b = foo)) [...]