1

I am trying to understand why the below function gives compilation error:

# Works fine
def test(a, b=10, c=20)
    p a, b, c
end

# Works fine
def test(a=10, b=20, c)
    p a, b, c
end

# Gives error - syntax error, unexpected '=', expecting ')'
def test(a=10, b, c=20)
    p a, b, c
end

It seems multiple default parameters do not work if they are all not either at beginning or end of parameter list.

Wand Maker
  • 18,476
  • 8
  • 53
  • 87
Noman Ur Rehman
  • 6,707
  • 3
  • 24
  • 39
  • 4
    I think `def test(foo = nil, bar, baz = nil)` sums up your question. – ndnenkov Sep 11 '15 at 18:15
  • 2
    There is major code-smell when you have multiple parameters to a method like you do. Instead, either define a structure, an object, or use a hash to define a wrapper or container for the options. This is really an XY problem: Your first question should be "How do I pass multiple required and optional parameters to a method." That answer would have solved this problem in advance. – the Tin Man Sep 11 '15 at 18:31
  • @theTinMan My objective here is to understand why the function definitions are not working. I know it's bad code :) – Noman Ur Rehman Sep 11 '15 at 18:33
  • 1
    Then please follow the guidelines about asking a question. Reduce your code to the bare minimum necessary to demonstrate the problem. You don't need ten parameters when two or three would work. – the Tin Man Sep 11 '15 at 18:36
  • @ndn Why that function definition gives compilation error anyways? – Wand Maker Sep 11 '15 at 18:46
  • @WandMaker, unfortunately, I don't know. I'm guessing there was an implementation obstacle and it wasn't worth working around it for something that probably has no practical implications. But still, it's a little ugly and inconsistent. I would definitely upvote whoever answers this question. – ndnenkov Sep 11 '15 at 18:49

2 Answers2

1

Ruby allows mandatory and optional parameters to be defined in a flexible manner. These all work and are logical:

def foo(a); end 
def foo(a, b); end 
def foo(a, b=1); end
def foo(a=1, b); end
def foo(a=1, b=1); end

If you need three or four parameters, start considering whether you should switch to a different way of passing in the parameters. It's a maintenance and readability thing.

Instead of anything that looks like

def test(param1=nil, param2, param3, param4, param5, param6, param7, param8, param9, param10) 

WAY before that point, switch to something more concise. Any of these would be preferable:

def foo(opts)
  a, b, c = opts.values_at(*%i[a b c]).map{ |v| v.nil? ? 'nil' : v }
  "a: '#{a}' b: '#{b}' c: '#{c}'"
end

foo(a:'a') # => "a: 'a' b: 'nil' c: 'nil'"
foo(a:'a', b:'b') # => "a: 'a' b: 'b' c: 'nil'"
foo(b:'b') # => "a: 'nil' b: 'b' c: 'nil'"
foo(a:'a', b:'b', c: 'c') # => "a: 'a' b: 'b' c: 'c'"

or:

Options = Struct.new('Options', 'a', 'b', 'c')

def foo(opts)
  a, b, c = opts.values.map{ |v| v.nil? ? 'nil' : v }
  "a: '#{a}' b: '#{b}' c: '#{c}'"
end

foo(Options.new('a', nil, 'c')) # => "a: 'a' b: 'nil' c: 'c'"
foo(Options.new('a', 'b', 'c')) # => "a: 'a' b: 'b' c: 'c'"
foo(Options.new(nil, 'b', 'c')) # => "a: 'nil' b: 'b' c: 'c'"

or:

require 'ostruct'

def foo(opts)
  a, b, c = opts.to_h.values_at(*%i[a b c]).map{ |v| v.nil? ? 'nil' : v }
  "a: '#{a}' b: '#{b}' c: '#{c}'"
end

foo(OpenStruct.new(:a => 'a')) # => "a: 'a' b: 'nil' c: 'nil'"
foo(OpenStruct.new(:a => 'a', :b => 'b')) # => "a: 'a' b: 'b' c: 'nil'"
foo(OpenStruct.new(:a => 'a', :b => 'b', :c => 'c')) # => "a: 'a' b: 'b' c: 'c'"

Using that sort of parameter passing greatly reduces the noise. Inside the method you can look for values that aren't initialized and force their default values and/or raise an error if you didn't receive a mandatory value.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
0

Paraphrasing the rules of parameter binding from this answer related to default parameters.

  1. As long as there are unbound mandatory parameters at the beginning of the parameter list, bind arguments left-to-right
  2. As long as there are unbound mandatory parameters at the end of the parameter list, bind arguments right-to-left
  3. Any leftover arguments are bound to optional parameters left-to-right
  4. Any leftover arguments are collected into an array and bound to the splat argument
  5. A block is wrapped up into a Proc and bound to the block argument
  6. If there are any unbound parameters or leftover arguments, raise an ArgumentError

If we read the above rules carefully, it seems to imply that:

If you don't have mandatory parameter(s) either at the beginning or end of parameter list, then, all parameters should be optional.

Ruby seems to be enforcing the above rules in the below function definition:

# Gives error - syntax error, unexpected '=', expecting ')'
def test(a=10, b, c=10)
    p a, b, c
end

If one really needs optional parameters at beginning and end, one can use something like below - it uses default parameters at beginning and keyword parameters at end.

def test(a=10, b=20, c, d: 40, e: 40)
    p a, b, c, d, e
end

test (30)
#=> 10, 20, 30, 40, 50

However, below will not work:

def test(a=10, b=20, c, d: 40, e: 40, f)
    p a, b, c, d, e
end
# Will produce below error
# syntax error, unexpected tIDENTIFIER
# def test(a=10, b=20, c, d: 40, e: 40, f)
                                        ^
Community
  • 1
  • 1
Wand Maker
  • 18,476
  • 8
  • 53
  • 87