Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does ruby only allow certain orderings of default parameters and splats?

Tags:

ruby

I came across a weird situation when coding ruby:

def foo(bar=true,baz,*args)
end

Raises this error:

syntax error, unexpected tSTAR

Whereas these:

def foo(bar,baz=true,*args)
end

def foo(bar=true,baz=true,*args)
end

are okay.

To add to the strangeness, these:

def foo(bar,baz=true)
end

def foo(bar=true,baz)
end

both work.

However, this:

def foo(bar=true,baz,args=true)
end

raises an error, but these:

def foo(bar=true,baz=true,args)
end

def foo(bar,baz=true,args=true)
end

are fine.

I figure there is a logical reason for why certain combinations are allowed and others aren't, but I can't find them anywhere, either through google or searching stackoverflow. My question is simply: Why are some of these combinations allowed while others aren't?

like image 629
NielMalhotra Avatar asked Dec 13 '25 08:12

NielMalhotra


1 Answers

Ruby books out there explain this in detail. But even without one, you will get used to the logic of it. A decent coder defines functions with compulsory ordered parameters going first, like this:

def foo( a, b, c=0, d=0 )
  # ...
end

Simply, first 2 args go to a, b, and up to 2 optional args to c, d, in that order. But you are granted freedom beyond common decency:

def foo( a=0, b )
  # ...
end

If you supply 1 argument, it goes to b. If you supply 2 args, they get assigned to a, b, in that order. This is less decent, because it takes more learning from the user. If you yourself are the user, you are being indecent to yourself, but still, there is no syntactic ambiguity at all. Now look at this:

def foo( a=0, b, c=0 )
  # ...
end

If you call foo( 1, 2 ), did you mean that a = 1, b = 2, c = 0, or a = 0, b = 1, c = 2? Perhaps first one is more likely, but the rule of coding is, do not guess the user's intentions. So, probably, Ruby team decided not to pamper this syntax. You can still achieve it by defining:

def foo( *args )
  a, b, c = case args.size
            when 1 then [0, args[0], 0]
            when 2 then [*args, 0]
            when 3 then args
            else fail ArgumentError; "#{args.size} arguments for 1..3!" end
  # ...
end

Thereby, in fact, you are granted absolute freedom, but Ruby guides you to good practice: By making good code look better than bad code. Same applies to collecting arguments with * splat. The decent case is unambiguous:

def foo( a, b=0, *c )
  # ...
end

But with:

def foo( a=0, b, *c )
  # ...
end

When you call foo( 1, 2, 3 ), did you mean that a = 1, b = 2, and c = [3], or a = 0, b = 1, and c = [2, 3]? Therefore, again, this second case is not encouraged, but nevertheless achievable:

def foo( *c )
  a, b = case c.size
         when 0 then fail ArgumentError, "Too few arguments!"
         when 1 then [0, c.shift]
         else [c.shift, c.shift] end
  # ...
end

Writing functions with many arguments, and with many ordered arguments in particular, is bad practice. There is an old saying, that binary method is better than ternary, unary better than binary, and finally, nullary one better than unary. If your method craves many arguments, you should refactor it by replacing it with an object. Does this answer your question?

like image 79
Boris Stitnicky Avatar answered Dec 15 '25 05:12

Boris Stitnicky