what's up with return *splat ?

Discussion in 'Ruby' started by Phlip, Jun 12, 2012.

  1. Phlip

    Phlip Guest


    Back in the Halcyon days (whatever that means) of Ruby 1.8, a function
    could obey the contract "return nil, a scalar, or an array" with a
    mere splat:

    return * splat

    The interpretation there is (roughly!) "splat behaves as if you had
    written each argument in an array as scalars separated by commas."

    So (roughly!), you would get one of these three results:

    return nil
    return scalar
    return [ scalar1, scalar2, scalar3 ]

    Now that I work in Ruby 1.9, the splat don't work like that. It just
    passes through an array.

    Am I using it wrong? Did splat change? If so, why? And can I use some
    other 1.9-compliant trick?
    Phlip, Jun 12, 2012
    1. Advertisements

  2. Can you please show the code you used for testing? Thank you.

    Kind regards

    Robert Klemme, Jun 12, 2012
    1. Advertisements

  3. Phlip

    Phlip Guest

    Can you please show the code you used for testing?  Thank you.

    My assert_latest() stopped working. Here's an independent test case:


    def assert(x); x or raise 'broke!'; end

    def splat(*wat); return *wat; end

    assert nil == splat()
    assert 'yo' == splat('yo')
    assert ['yo', 'dude'] == splat('yo', 'dude')

    assert [] == splat()
    assert ['yo'] == splat('yo')
    assert ['yo', 'dude'] == splat('yo', 'dude')

    The top three assertions pass in 1.8.7, and the bottom three pass in
    Phlip, Jun 13, 2012
  4. I get different results:

    $ ruby x.rb
    broke! x.rb:12
    broke! x.rb:13
    $ ruby19 x.rb
    broke! x.rb:8:in `<main>'
    broke! x.rb:9:in `<main>'
    $ cat -n x.rb
    4 def assert(x); x or warn "broke! #{caller[0]}"; end
    6 def splat(*wat); return *wat; end
    8 assert nil == splat()
    9 assert 'yo' == splat('yo')
    10 assert ['yo', 'dude'] == splat('yo', 'dude')
    12 assert [] == splat()
    13 assert ['yo'] == splat('yo')
    14 assert ['yo', 'dude'] == splat('yo', 'dude')
    16 a,b=splat(1,2)
    17 assert a == 1
    18 assert b == 2
    20 a,b=splat(1)
    21 assert a == 1
    22 assert b == nil
    24 a,b=splat
    25 assert a == nil
    26 assert b == nil

    And the interesting bits (lines 16ff) are treated identical.

    I think you use a function in one of two ways: either you expect one
    result and that can be nil or not, or you expect multiple replies and
    assign them to different variables which can either be nil or not.

    What practical use case is impaired by the difference?

    Kind regards

    Robert Klemme, Jun 16, 2012
  5. Phlip

    Phlip Guest

    Tx for the experiment; it confirmed mine.
    When I write an assertion, I know the ordinality of the expected

    note1, note2 = assert_latest User.notes do

    assert{ user.notes == [note1, note2] }

    frob = assert_latest Frob do

    assert{ frob.member == 42 }

    I want the assertions to break, with simple syntax errors, if the
    ordinality is wrong. If assert_latest only returned arrays, that would
    cause clutter like this:

    frobs = assert_latest Frob do

    assert{ frobs.count == 1 and frobs[0].member == 42 }

    Instead of testing the count, I want to simply use the result as if
    it's what I expect, and then fail as early as possible if it isn't.

    When I first asked this question (on these very newsgroups IIRC), I
    was directed to return *records. That solved the ton of crud required
    to put the ordinality check _inside_ assert_latest().

    Then return *splat's behavior changed in 1.9.2.
    Phlip, Jun 22, 2012
  6. Strange. I said
    You cannot get syntax errors for this. This is checked at runtime and
    not at parse time.
    Note that you can do this in 1.9.* and 1.8.7 (the comma):

    irb(main):002:0> def f(*a) a end
    => nil
    irb(main):003:0> f 1,2
    => [1, 2]
    irb(main):004:0> x=f 1,2
    => [1, 2]
    irb(main):005:0> x
    => [1, 2]
    irb(main):006:0> x,=f 1,2
    => [1, 2]
    irb(main):007:0> x
    => 1

    Kind regards

    Robert Klemme, Jun 22, 2012
  7. Phlip

    Phlip Guest

    Your experiment confirmed 1.9.2 changed the behavior, like mine did.
    I'm aware of the definition of syntax error.
    That line does not die, with an-error-checked-at-runtime, nor would
    x.member (given [] does not have .member) die with an-error-checked-at-

    When the author of a test writes assert_latest() they know whether
    they expect a scalar or a list to return, and they should not waste
    their time checking for a single-element list before using it in the
    assertion that actually checks for the important stuff.

    If I use record, = assert_latest(), and if a future bug added a second
    record, the assertion would not break, that line would not break (with
    a comma), and lines using record would not break.

    Also I'm not sure why I need to justify the use case, if such a low-
    level syntactical thing should not change in a language revision. It's
    a bug in return *splat; it no longer behaves the same as = *splat.
    Phlip, Jun 22, 2012
  8. Yes, but I got different results than those you claimed. Which made me
    Why then do you use it in the wrong way?
    Well, if you use "x,=..." then you have the first element and no
    additional checking is needed. But if you really want to ensure the
    proper number of values is returned you need to test for the array
    length anyway. At least you would need to to something like

    irb(main):008:0> x,y,*remainder = f 1,2
    => [1, 2]
    irb(main):009:0> remainder
    => []
    irb(main):010:0> remainder.empty?
    => true
    irb(main):011:0> x,y,*remainder = f 1,2,3
    => [1, 2, 3]
    irb(main):012:0> remainder.empty?
    => false

    For consistent checking of the returned data you can do

    *x = f(...)

    x will be an Array even if f returns a single value only.
    Actually most people seem to cope pretty well with this as only corner
    cases are affected of the change. If you want to ensure no additional
    values are returned you need to splat assign any way to count values, i.e.

    *x = f()
    It's not syntax but runtime behavior. You can and will never get a
    syntax error for this.

    AFAIK Matz changed it deliberately - although I have to confess I don't
    remember the reasoning. You'll probably find it in the archives.

    Note though that also argument assignment changed quite a bit in 1.9
    which has much more sophisticated options where 1.8 only allowed for the
    last parameter to collect additional values. The 1.9 model is superior
    to that:

    irb(main):001:0> def f(a, *b, c) p a,b,c end
    => nil
    irb(main):002:0> f 1
    ArgumentError: wrong number of arguments (1 for 2)
    from (irb):1:in `f'
    from (irb):2
    from /usr/local/bin/irb19:12:in `<main>'
    irb(main):003:0> f 1,2
    => [1, [], 2]
    irb(main):004:0> f 1,2,3
    => [1, [2], 3]
    irb(main):005:0> f 1,2,3,4,5
    [2, 3, 4]
    => [1, [2, 3, 4], 5]

    And you have pattern matching with block arguments

    irb(main):007:0> f = lambda {|a, (b,c), d| p a,b,c,d}
    => #<Proc:[email protected](irb):7 (lambda)>
    irb(main):008:0> f[1]
    ArgumentError: wrong number of arguments (1 for 3)
    from (irb):7:in `block in irb_binding'
    from (irb):8:in `[]'
    from (irb):8
    from /usr/local/bin/irb19:12:in `<main>'
    irb(main):009:0> f[1,2]
    ArgumentError: wrong number of arguments (2 for 3)
    from (irb):7:in `block in irb_binding'
    from (irb):9:in `[]'
    from (irb):9
    from /usr/local/bin/irb19:12:in `<main>'
    irb(main):010:0> f[1,2,3]
    => [1, 2, nil, 3]
    irb(main):011:0> f[1,[2],3]
    => [1, 2, nil, 3]
    irb(main):012:0> f[1,[2,3],4]
    => [1, 2, 3, 4]
    irb(main):013:0> f[1,[2,3,4],5]
    => [1, 2, 3, 5]


    Robert Klemme, Jun 22, 2012
  9. Phlip

    Phlip Guest

    Actually most people seem to cope pretty well with this as only corner
    Sorry, I can't handle this level of talking-past each other.
    Phlip, Jun 25, 2012
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.