Parallel Assignments and Elegance/Complexity Ratio

In SICP, I read that “Programs should be written for people to read, and
only incidentally for machines to execute”.

While reading David/Matz book, I stumbled upon parallel assignments and
I thought the language was trying to be too flexible (adding complexity,
at least for a newcomer). My head soon started
spinning (when it reached a, (b, (c,d)) = 1, [2, [3, 4]] I was
exhausted).

My experience is recorded here:

Summary is, I should only spend time learning

  • x, y, z = 1, 2, 3 (# => x=1, y=1, z=3), and
  • x, y = y, x (# => swap x and y)

I gather that this might be a matter of taste and style, but are other
variants used by community?

Thank you,
Kedar

On Tue, Jan 11, 2011 at 3:29 PM, Kedar M.
[email protected] wrote:

https://docs1.google.com/document/d/1zpHvfO4be3UvjaxU7L5gEPXFMENcMmEz9qqIcecEzOg/edit?hl=en#

Summary is, I should only spend time learning

  • x, y, z = 1, 2, 3 (# => x=1, y=1, z=3), and
  • x, y = y, x (# => swap x and y)

I gather that this might be a matter of taste and style, but are other
variants used by community?

Just today I used

def []=(*idx, val)
index_check(idx)
@data[idx] = val
end

https://gist.github.com/772827

See also thread “Nooby question : multidimensional arrays.”.

I find the pattern matching very elegant. This also comes in handy
in situations like this:

irb(main):001:0> a = Array.new(10) { [rand(10), rand(10)] }
=> [[2, 3], [7, 8], [4, 0], [4, 6], [5, 2], [9, 9], [4, 2], [8, 5],
[2, 6], [5, 9]]
irb(main):002:0> a.sort {|(a1,a2),(b1,b2)| x = a2 <=> b2; x == 0 ? a1
<=> b1 : x}
=> [[4, 0], [4, 2], [5, 2], [2, 3], [8, 5], [2, 6], [4, 6], [7, 8],
[5, 9], [9, 9]]

Although in this particular case you could also use

irb(main):004:0> a.sort_by {|arr| arr.reverse}
=> [[4, 0], [4, 2], [5, 2], [2, 3], [8, 5], [2, 6], [4, 6], [7, 8],
[5, 9], [9, 9]]
irb(main):005:0> a.sort_by {|x, y| [y, x]}
=> [[4, 0], [4, 2], [5, 2], [2, 3], [8, 5], [2, 6], [4, 6], [7, 8],
[5, 9], [9, 9]]

Generally I’d say I don’t use it overly often but when I use it it is
really handy. With #inject it’s also useful if you iterate through a
collection of Arrays of known equal length:

irb(main):006:0> a.inject(0) {|s,(x,y)| s + x + y}
=> 100

Kind regards

robert

Robert K. wrote in post #973945:

On Tue, Jan 11, 2011 at 3:29 PM, Kedar M.
[email protected] wrote:

https://docs1.google.com/document/d/1zpHvfO4be3UvjaxU7L5gEPXFMENcMmEz9qqIcecEzOg/edit?hl=en#

Summary is, I should only spend time learning

  • x, y, z = 1, 2, 3 (# => x=1, y=1, z=3), and
  • x, y = y, x (# => swap x and y)

I gather that this might be a matter of taste and style, but are other
variants used by community?

Just today I used

def []=(*idx, val)
index_check(idx)
@data[idx] = val
end

https://gist.github.com/772827

See also thread “Nooby question : multidimensional arrays.”.

I find the pattern matching very elegant. This also comes in handy
in situations like this:

Maybe I am not understanding it, but I thought that elegance is because
of splat operator (which I am sure I like). My gripe is about various
forms of parallel assignment and semantic/syntactic complexity because
of that. Or are you saying that once you say you need splat operator,
all this complexity is inevitable (of course, I can work around it by
not using it, but then how do you all use it?)

-Kedar

irb(main):001:0> a = Array.new(10) { [rand(10), rand(10)] }
=> [[2, 3], [7, 8], [4, 0], [4, 6], [5, 2], [9, 9], [4, 2], [8, 5],
[2, 6], [5, 9]]
irb(main):002:0> a.sort {|(a1,a2),(b1,b2)| x = a2 <=> b2; x == 0 ? a1
<=> b1 : x}
=> [[4, 0], [4, 2], [5, 2], [2, 3], [8, 5], [2, 6], [4, 6], [7, 8],
[5, 9], [9, 9]]

Although in this particular case you could also use

irb(main):004:0> a.sort_by {|arr| arr.reverse}
=> [[4, 0], [4, 2], [5, 2], [2, 3], [8, 5], [2, 6], [4, 6], [7, 8],
[5, 9], [9, 9]]
irb(main):005:0> a.sort_by {|x, y| [y, x]}
=> [[4, 0], [4, 2], [5, 2], [2, 3], [8, 5], [2, 6], [4, 6], [7, 8],
[5, 9], [9, 9]]

Generally I’d say I don’t use it overly often but when I use it it is
really handy. With #inject it’s also useful if you iterate through a
collection of Arrays of known equal length:

irb(main):006:0> a.inject(0) {|s,(x,y)| s + x + y}
=> 100

Kind regards

robert

On Tue, Jan 11, 2011 at 8:29 AM, Kedar M.
[email protected]wrote:

Kedar


Posted via http://www.ruby-forum.com/.

I don’t use it very often, but when I do, it usually makes an elegant
solution. I think part of the reason it doesn’t seem that way is because
you
are playing with it in too sterile of an environment. For example, you
rate
“x, (y, (z, a))=[1, [2, [3, 4]]]” as lowest, suggesting it is equivalent
to
“x=1;y=2;z=3;a=4” but this is not true. If you are actually assigning
with
literals, you would, of course, use the equivalent way, but if your data
comes in as nested arrays, then you can’t assign like that, instead you
have
to do something like this:

def parallel(values)
x, (y, (z, a))=values
[x,y,z,a]
end
def alternative(values)
x = values.shift
values = values.shift
y = values.shift
values = values.shift
z = values.shift
a = values.shift
[x,y,z,a]
end
parallel [1, [2, [3, 4]]] # => [1, 2, 3, 4]
alternative [1, [2, [3, 4]]] # => [1, 2, 3, 4]

Now, I don’t normally store data like that, so I haven’t ever done
anything
quite that fancy, but I use arrays on the RHS on occasion. It might look
something like this (though I don’t normally store my data like this,
either
– it’s really hard to think of a decent example!).

$stdin = DATA
while input = gets
name , num = input.split
puts “#{name.capitalize}'s favourite number is #{num}”
end
END
josh 12
bill 42
sally 13
ned 99
clara 1000000

The alternative of
name , num = input.split
is
values = input.split
name = values.shift
num = values

I consider the former to be much more elegant as it avoids a temporary
variable.

On Tue, Jan 11, 2011 at 2:29 PM, Kedar M.
[email protected]wrote:

variable.
I consider the former to be much more elegant as it avoids a temporary
This however is already covered by me as the first case (with HIGH
elegance rating) since you are expecting the line to contain exactly two
strings (and if it is not so, it’s an exceptional situation) and hence
the (expected) number of lvalues = number of rvalues. I do
like/understand such application of parallel assignment.

Sorry, I don’t understand. In this example, the rhs values are contained
in
an Array, which you consider low elegance, in your first example the rhs
values are discrete. I don’t see how they are the same, I think this is
an
instance of your “x, y, z = [1, 2, 3]”, which you consider to be low
elegance.

Sorry, I don’t understand. In this example, the rhs values are contained
in
an Array, which you consider low elegance, in your first example the rhs
values are discrete. I don’t see how they are the same, I think this is
an
instance of your “x, y, z = [1, 2, 3]”, which you consider to be low
elegance.

Ah, you are right. I stand corrected. Thanks.

literals, you would, of course, use the equivalent way, but if your data
comes in as nested arrays, then you can’t assign like that, instead you
have
to do something like this:

def parallel(values)
x, (y, (z, a))=values
[x,y,z,a]
end
def alternative(values)
x = values.shift
values = values.shift
y = values.shift
values = values.shift
z = values.shift
a = values.shift
[x,y,z,a]
end
parallel [1, [2, [3, 4]]] # => [1, 2, 3, 4]
alternative [1, [2, [3, 4]]] # => [1, 2, 3, 4]

Now, I don’t normally store data like that, so I haven’t ever done
anything
quite that fancy, but I use arrays on the RHS on occasion. It might look
something like this (though I don’t normally store my data like this,
either
– it’s really hard to think of a decent example!).

Thank you for sharing the use case. In this case, it helps. But like you
said, it seems like an answer in search of a question.

$stdin = DATA
while input = gets
name , num = input.split
puts “#{name.capitalize}'s favourite number is #{num}”
end

The alternative of
name , num = input.split
is
values = input.split
name = values.shift
num = values

I consider the former to be much more elegant as it avoids a temporary
variable.
This however is already covered by me as the first case (with HIGH
elegance rating) since you are expecting the line to contain exactly two
strings (and if it is not so, it’s an exceptional situation) and hence
the (expected) number of lvalues = number of rvalues. I do
like/understand such application of parallel assignment.

Thanks, again!

On Tue, Jan 11, 2011 at 7:54 PM, Kedar M.
[email protected] wrote:

https://gist.github.com/772827

See also thread “Nooby question : multidimensional arrays.”.

I find the pattern matching very elegant. This also comes in handy
in situations like this:

Maybe I am not understanding it, but I thought that elegance is because
of splat operator (which I am sure I like).

The splat operator is just part of the game as Josh has tried to
demonstrate. The real power comes from pattern matching which will
even work with multiple levels of nesting. And the mechanism is the
same for method and block arguments which gives you one powerful
mechanism usable in several places.

My gripe is about various
forms of parallel assignment and semantic/syntactic complexity because
of that. Or are you saying that once you say you need splat operator,
all this complexity is inevitable (of course, I can work around it by
not using it, but then how do you all use it?)

No, I am saying that parallel assignment is just a special case of the
assignment mechanism that is also in effect for method and block
arguments. And while I don’t use the fancy variants often (as Josh)
when I use it it yields an elegant solution that would be more
cumbersome without it.

Kind regards

robert

On Tue, Jan 11, 2011 at 3:30 PM, Robert K.
[email protected] wrote:

… I find the pattern matching very elegant. … Generally I’d say
I don’t use it overly often but when I use it it is really handy. …

On Tue, Jan 11, 2011 at 7:58 PM, Josh C. [email protected]
wrote:

I don’t use it very often, but when I do, it usually makes an
elegant solution. I think part of the reason it doesn’t seem that
way is because you are playing with it in too sterile of an environment.

That last sentence seems an accurate assessment of why it perhaps
doesn’t seem much use to you, and what Josh C. and Robert K.
say about those situations where they use it seems appropriate,
although I haven’t personally used it in those types of situations
because my Ruby use is relatively simple.

On Tue, Jan 11, 2011 at 8:29 PM, Kedar M.
[email protected] wrote:

… This however is already covered by me as the first case (with HIGH
elegance rating) since you are expecting the line to contain exactly two
strings … and hence the (expected) number of lvalues = number of rvalues.
I do like/understand such application of parallel assignment.

Strangely, that’s the situation (number of lvalues == number of
rvalues) when I definitely dislike its use (unless it might be
necessary to ensure all the rvalues are calculated before any
assignment takes place - might side effects make this necessary?), for
two (and a quarter) reasons.

First, whenever I’ve benchmarked parallel assignment against
individual assignment, I’ve found the parallel assignment somewhat
slower. The swap is more elegant, but even that seems slower.

Second, parallel assignment can make it difficult to see what is being
assigned to what. In the following, is it instantly obvious what e is
being set to?

a, b, c, d, e, f, g, h = q, r, s, t, u, v, w, x

If it is instantly obvious, try more l and r values and/or try longer
variable names and/or values and/or split the assignment expression
over multiple lines!

As an actual example, this from Date:
def initialize(ajd=0, of=0, sg=ITALY)
@ajd, @of, @sg = ajd, of, sg
# …
end
Admittedly it’s easy here to see what’s being assigned to what, but
even so is that really better than the alternative just below? Is
there a reason for using the parallel assignment in the actual
example, other than aesthetics?
def initialize(ajd=0, of=0, sg=ITALY)
@ajd = ajd; @of = of; @sg = sg
# …
end

The quarter is the positive side of the negative point of the second,
that explicit individual assignment is easier visually/cognitively
than parallel assignment.

*** benchmarks

require “benchmark”
a = b = c = d = e = f = g = h = nil; x = y = nil
kt = 1_000_000
Benchmark.bm( 22 ) do |bm|
bm.report( “parallel assignment” ) { kt.times{
a, b, c, d, e, f, g, h = 17, 23, 13, 48, 42, 46, 26, 24 } }
bm.report( “individual assignment” ) { kt.times{
a = 17; b = 23; c = 13; d = 48; e = 42; f = 46; g = 26; h = 24 } }
bm.report( “swap elegant” ) { kt.times{
x = 113; y = 355; x, y = y, x } }
bm.report( “swap messy” ) { kt.times{
x = 113; y = 355; z = x; x = y; y = z } }
end

Using Linux, Ruby 1.9.1, but I’ve had similar results on MS Windows.
user system total real
parallel assignment 0.630000 0.000000 0.630000 ( 0.635576)
individual assignment 0.400000 0.000000 0.400000 ( 0.393652)
swap elegant 0.500000 0.000000 0.500000 ( 0.506171)
swap messy 0.300000 0.000000 0.300000 ( 0.298167)

MS Windows: ruby 1.9.1p430 (2010-08-16 revision 28998) [i386-mingw32]
user system total real
parallel assignment 0.531000 0.000000 0.531000 ( 0.530000)
individual assignment 0.327000 0.000000 0.327000 ( 0.335000)
swap elegant 0.500000 0.000000 0.500000 ( 0.490000)
swap messy 0.296000 0.000000 0.296000 ( 0.300000)

MS Windows: jruby 1.5.3 (ruby 1.8.7 patchlevel 249) (2010-09-28 7ca06d7)
(Java HotSpot™ Client VM 1.6.0_14) [x86-java]
user system total real
parallel assignment 0.735000 0.000000 0.735000 ( 0.705000)
individual assignment 0.460000 0.000000 0.460000 ( 0.460000)
swap elegant 0.560000 0.000000 0.560000 ( 0.560000)
swap messy 0.425000 0.000000 0.425000 ( 0.425000)

I do like/understand such application of parallel assignment.

Strangely, that’s the situation (number of lvalues == number of
rvalues) when I definitely dislike its use (unless it might be
necessary to ensure all the rvalues are calculated before any
assignment takes place - might side effects make this necessary?), for
two (and a quarter) reasons.

Interesting (yeah, it’s a matter of taste).
But when number of variables is <=3, it looks very readable and easily
understandable.

First, whenever I’ve benchmarked parallel assignment against
individual assignment, I’ve found the parallel assignment somewhat
slower. The swap is more elegant, but even that seems slower.

That’s a good point, but it’s judgmental based on how frequently the
code gets called.

a, b, c, d, e, f, g, h = q, r, s, t, u, v, w, x

If it is instantly obvious, try more l and r values and/or try longer
variable names and/or values and/or split the assignment expression
over multiple lines!

Well, one can always abuse a feature. I think number of variables should
be <=3, e.g.
a, b, c = q, r ,s
@d, @e, @f = t(a), u(b), v© # …

As an actual example, this from Date:
def initialize(ajd=0, of=0, sg=ITALY)
@ajd, @of, @sg = ajd, of, sg
# …
end
Admittedly it’s easy here to see what’s being assigned to what, but
even so is that really better than the alternative just below? Is
there a reason for using the parallel assignment in the actual
example, other than aesthetics?
def initialize(ajd=0, of=0, sg=ITALY)
@ajd = ajd; @of = of; @sg = sg
# …
end

The quarter is the positive side of the negative point of the second,
that explicit individual assignment is easier visually/cognitively
than parallel assignment.

Yeah. And the (others’) wisdom suggests that if and when you find a
situation to apply it, it (usually) renders an elegant solution.

-Kedar

On Wed, Jan 12, 2011 at 10:44 AM, Colin B.
[email protected] wrote:

First, whenever I’ve benchmarked parallel assignment against
individual assignment, I’ve found the parallel assignment somewhat
slower. The swap is more elegant, but even that seems slower.

Parallel assignment is generally slower than straight-up assignment in
1.9 and JRuby because it stands up a full Ruby Array for the RHS and
result of the entire assignment expression:

~/projects/jruby ➔ jruby -e “p((a, b, c = 1, 2, 3))”
[1, 2, 3]

As you would expect this is a significant cost compared to just
assigning the values directly. JRuby can improve this when it knows
that the assignment is not being used as an expression:

~/projects/jruby ➔ jruby -rbenchmark -e “2.times{ puts
Benchmark.measure { 10000000.times {a,b,c=1,2,3} } }”
1.924000 0.000000 1.924000 ( 1.812000)
1.810000 0.000000 1.810000 ( 1.810000)

~/projects/jruby ➔ jruby -rbenchmark -e “2.times{ puts
Benchmark.measure { 10000000.times {a,b,c=1,2,3; nil} } }”
1.073000 0.000000 1.073000 ( 0.959000)
0.884000 0.000000 0.884000 ( 0.884000)

There are also some implementations that “cheat” (I mean that in the
nicest way possible) and don’t bother producing that array return
value at all, and they perform much better on parallel assignment as a
result.

  • Charlie

On Fri, Jan 14, 2011 at 2:45 PM, Colin B.
[email protected] wrote:

Now the only thing that’s puzzling me is why the MRI 1.9.1 “cheating
honestly” version of parallel assignment seems to be slightly but
clearly faster than the MRI 1.9.1 single assignment!

Yes, that is a bit baffling! I have no explanation for that. As you
can see in JRuby, the times for the non-expression parallel assignment
and the normal assignment are roughly the same.

1.561000 0.000000 1.561000 ( 1.562000)
1.558000 0.000000 1.558000 ( 1.558000)

FWIW, you’d get better results here if you ran a couple iterations,
and of course if you specified --server it’s significantly better…

~/projects/jruby ➔ jruby -v passign.rb
jruby 1.6.0.RC1 (ruby 1.8.7 patchlevel 330) (2011-01-14 da2bb9d) (Java
HotSpot™ Client VM 1.6.0_22) [darwin-i386-java]
1.952000 0.000000 1.952000 ( 1.839000)
1.784000 0.000000 1.784000 ( 1.784000)
1.796000 0.000000 1.796000 ( 1.796000)
0.919000 0.000000 0.919000 ( 0.919000)
0.865000 0.000000 0.865000 ( 0.865000)
0.856000 0.000000 0.856000 ( 0.856000)
0.961000 0.000000 0.961000 ( 0.961000)
0.924000 0.000000 0.924000 ( 0.924000)
0.880000 0.000000 0.880000 ( 0.880000)

~/projects/jruby ➔ jruby --server -v passign.rb
jruby 1.6.0.RC1 (ruby 1.8.7 patchlevel 330) (2011-01-14 da2bb9d) (Java
HotSpot™ Server VM 1.6.0_22) [darwin-i386-java]
1.388000 0.000000 1.388000 ( 1.324000)
1.086000 0.000000 1.086000 ( 1.086000)
1.034000 0.000000 1.034000 ( 1.034000)
0.522000 0.000000 0.522000 ( 0.522000)
0.500000 0.000000 0.500000 ( 0.500000)
0.491000 0.000000 0.491000 ( 0.491000)
0.517000 0.000000 0.517000 ( 0.517000)
0.485000 0.000000 0.485000 ( 0.485000)
0.496000 0.000000 0.496000 ( 0.496000)

On Fri, Jan 14, 2011 at 4:41 PM, Charles Oliver N.
[email protected] wrote:

Parallel assignment is generally slower than straight-up assignment in
1.9 and JRuby because it stands up a full Ruby Array for the RHS and
result of the entire assignment expression:

I’d assumed it was something like that. Thanks for the explanation.

As you would expect this is a significant cost compared to just
assigning the values directly. JRuby can improve this when it knows
that the assignment is not being used as an expression:

There are also some implementations that “cheat” (I mean that in the
nicest way possible)

I don’t know if you’ve heard of the English magician Paul D.s, but
I’m very fond of a phrase he uses to contrast himself with some people
who are, let us say, not self-admitted magicians. “I cheat, but I
cheat honestly”.

and don’t bother producing that array return value at all,
and they perform much better on parallel assignment as a result.

Cheating honestly, I think!

That’s interesting, and the “cheating” idea hadn’t ocurred to me. From
the benchmarks below (similar to those you quoted; I did actually run
the benchmarks twice, but the runs were very similar, so to avoid
clutter I’ve only given one run) it seems MRC 1.9.1 is also cheating
honestly if it can.

Now the only thing that’s puzzling me is why the MRI 1.9.1 “cheating
honestly” version of parallel assignment seems to be slightly but
clearly faster than the MRI 1.9.1 single assignment!

require “benchmark”
kt = 10_000_000
nn = 1
nn.times{ puts Benchmark.measure { kt.times {a,b,c=1,2,3 } } }
nn.times{ puts Benchmark.measure { kt.times {a,b,c=1,2,3; nil} } }
nn.times{ puts Benchmark.measure { kt.times {a = 1; b = 2; c = 3 } } }

jruby 1.5.3 (ruby 1.8.7 patchlevel 249) (2010-09-28 7ca06d7)
(Java HotSpot™ Client VM 1.6.0_14) [x86-java]
3.038000 0.000000 3.038000 ( 3.007000)
1.561000 0.000000 1.561000 ( 1.562000)
1.558000 0.000000 1.558000 ( 1.558000)

ruby 1.9.1p430 (2010-08-16 revision 28998) [i386-mingw32]
3.962000 0.000000 3.962000 ( 3.963000)
1.747000 0.000000 1.747000 ( 1.741000)
2.106000 0.000000 2.106000 ( 2.105000)

Which avoids making you look like your writing in LISP.
These two bits of code are not the same thing.

a, (b, c) = [1, [2, [3, 4]]]

a #=> 1
b #=> 2
c #=> [3, 4]

a, b, c = [1, [2, [3, 4]]].flatten

a #=> 1
b #=> 2
c #=> 3

Josh C. wrote in post #974019:

On Tue, Jan 11, 2011 at 8:29 AM, Kedar M.
[email protected]wrote:

Kedar


Posted via http://www.ruby-forum.com/.

I don’t use it very often, but when I do, it usually makes an elegant
solution. I think part of the reason it doesn’t seem that way is because
you
are playing with it in too sterile of an environment. For example, you
rate
“x, (y, (z, a))=[1, [2, [3, 4]]]” as lowest, suggesting it is equivalent
to
“x=1;y=2;z=3;a=4” but this is not true. If you are actually assigning
with
literals, you would, of course, use the equivalent way, but if your data
comes in as nested arrays, then you can’t assign like that, instead you
have
to do something like this:

def parallel(values)
x, (y, (z, a))=values
[x,y,z,a]
end

Instead you could also do:

x, y, z, a = values.flatten

Which avoids making you look like your writing in LISP.

For small stuff parrallel assignment makes it look elegant, but like any
language feature it can be abused. Although I don’t think I’ve ever
actually found a case where I’ve wanted to use this. Especially when
most of the time my ‘x, y, z and a’ variables are unrelated so I don’t
want to use them together in the same statement.