From: Daniel Berger Date: 2006-03-03T02:30:34+09:00 Subject: Re: Confusion Over Keyword Arguments Yukihiro Matsumoto wrote: > Hi, > > In message "Re: Confusion Over Keyword Arguments" > on Fri, 3 Mar 2006 00:57:44 +0900, "Berger, Daniel" writes: > > |That can be made to work, with the understanding that '=' in a method > |call means 'keyword argument', not 'assignment', since there is no point > |in doing an assignment in a method call. > > I'm not sure if we _can_. yacc is a tough guy to fight with. > > |http://djberg96.livejournal.com/50162.html > > In this article, you've proposed *rest to slurp keyword hash. Indeed > it is simple, but maybe too simpler. I don't get how it can co-exist > with _your_ ideal keyword parameters. I.e. how the following code > should work? > > def foo(a, b=0, *c) > ... > end > > foo(1, foo:3) # (a=1, b={:foo=>3}) or (a=1,b=0,c=[{:foo=>3}])? Error. There's no 'foo' parameter. Passing a literal hash would require {}. > foo(1, 2, 8, c:5) # c={:c=>5}) or c=[8,{:c=>5}] or error? Hm. Either an error or c = [5], depending on whether or not your want to declare that, once a positional is used, there's no going back after the fact. > args = [1, {:b=>2, foo=>5}] > foo(*args) # (a=1,b={:b=>2, foo=>5}) or (a=1,b=2,c=[{:foo=>5}])? a = 1, b = {:b=>2, foo=>5}, since you've passed a literal hash. Here's the latest test case we had for Sydney. Note that Sydney assumes that all method arguments automatically become keyword arguments. There's no syntax for explicitly declaring that a given method parameter is a valid keyword argument. For the kids following along at home, also see http://redhanded.hobix.com/inspect/namedParametersArenTTheyAllNamed.html for further discussion on the subject. Regards, Dan # # Tests the keyword argument behavior. # # Authored by Dan Berger and Evan Webb # require 'behavior/keyword' require 'test/unit' # This is the class we use within the test case below. class Foo MYCONST = 5 behavior KeywordBehavior attr_reader :x, :y, :z def bar(x, y, z=3) @x = x @y = y @z = z end def grab(x, y, *z) @x = x @y = y @z = z end def vars [@x, @y, @z] end def t2(x) name = "blah" end end # Added to test a method redefinition in a subclass, as well as the # define_method and instance_method methods. class Bar < Foo attr_reader :a, :b, :c def bar(a, b=2, c=3, d=4) @a = a @b = b @c = c end define_method(:baz, instance_method(:bar)) end TOPLEVEL=5 class TC_Arguments < Test::Unit::TestCase def setup @foo = Foo.new @bar = Bar.new @arr = [1,2,3] end def test_methods_defined assert_respond_to(@foo, :bar) assert_respond_to(@bar, :bar) assert_respond_to(@foo, :grab) assert_respond_to(@foo, :vars) assert_respond_to(@foo, :t2) assert_respond_to(@bar, :baz) end # Methods may still take positional arguments, the way they always have def test_positional_basic assert_nothing_raised{ @foo.bar(1,2) } assert_nothing_raised{ @foo.bar(1,2,3) } end def test_positional_basic_subclass assert_nothing_raised{ @bar.bar(1) } assert_nothing_raised{ @bar.bar(1,2) } assert_nothing_raised{ @bar.bar(1,2,3) } assert_nothing_raised{ @bar.bar(1,2,3,4) } assert_nothing_raised{ @bar.baz(1,2,3) } end # You can use named parameters, using the name of the parameter defined in # in the method itself. It must be the name of the parameter, followed by # a colon, followed by the value. Spaces between the parameter name, colon # and value are not allowed. def test_named_basic assert_nothing_raised{ @foo.bar(z:1, x:2, y:3) } assert_equal(2, @foo.x) assert_equal(3, @foo.y) assert_equal(1, @foo.z) end def test_named_basic_subclass assert_nothing_raised{ @bar.bar(d:1, a:2, b:3, c:4) } assert_equal(2, @bar.a) assert_equal(3, @bar.b) assert_equal(4, @bar.c) assert_equal(1, @bar.d) end # Ensure that named parameters in a define_method/instance_method work. # In this case, Bar#baz is actually calling Foo#bar. def test_named_dynamically_defined assert_nothing_raised{ @bar.baz(z:3, y:2, x:1) } assert_equal(1, @bar.x) assert_equal(2, @bar.y) assert_equal(3, @bar.z) end # Default values in a declaration work the same way they always have. The # use of named parameters does not change this. def test_named_basic_default_values assert_nothing_raised{ @foo.bar(x:1, y:2) } assert_equal(1, @foo.x) assert_equal(2, @foo.y) assert_equal(3, @foo.z) # default end # You can mix and match positional and named parameters, though there are # limitations. Positional parameters must come first (i.e. on the left # side). def test_mixed_basic assert_nothing_raised{ @foo.bar(1, z:2, y:3) } assert_equal(1, @foo.x) assert_equal(3, @foo.y) assert_equal(2, @foo.z) assert_nothing_raised{ @foo.bar(1, 2, z:4) } assert_equal(1, @foo.x) assert_equal(2, @foo.y) assert_equal(4, @foo.z) end # You can splat an array, and the values will be assigned in a left to right # fashion, as in the current Ruby behavior. def test_splat assert_nothing_raised{ @foo.bar(*@arr) } assert_equal(1, @foo.x) assert_equal(2, @foo.y) assert_equal(3, @foo.z) end # You can mix and match positional, named and splat arguments. However, # splat arguments must come last. This is the same as the current behavior # in Ruby. def test_mixed_splat a = [1] assert_nothing_raised{ @foo.bar(*a) } assert_nothing_raised{ @foo.bar(1, *a) } assert_nothing_raised{ @foo.bar(1, y:2, *a)} assert_nothing_raised{ @foo.bar(1, 2, *a) } assert_nothing_raised{ @foo.bar(x:1, y:2, *a) } end # The '*rest' declarations work the same as they always have. In the case # of named parameters, the values assigned to the 'rest' argument are # simply pushed as an array. def test_def_splat assert_nothing_raised{ @foo.grab(1, 2, 3, 4) } assert_equal([1, 2, [3, 4]], @foo.vars) assert_nothing_raised{ @foo.grab(x:1, y:2, z:4) } assert_equal([1, 2, [4]], @foo.vars) assert_nothing_raised{ @foo.grab(1, 2, 3, [1,2,3]) assert_equal([1,2,3,[[1,2,3]]], @foo.vars) assert_nothing_raised{ @foo.grab(1, 2, 3, z:[1,2,3]) assert_equal([1,2,3,[[1,2,3]]], @foo.vars) assert_nothing_raised{ @foo.grab(1, 2, 3, z:*[1,2,3]) assert_equal([1,2,3,[1,2,3]], @foo.vars) end # If a keyword argument is assigned to a '*rest' parameter, then it # autovivifies a hash, assigning that hash as the argument to the # '*rest' parameter. Note double declarations are still illegal. def test_keyword_in_splat assert_nothing_raised{ @foo.grab(10, 20, name:"evan", age:99) } assert_equal([ 10, 20, [{:name => "evan", :age=>99}] ], @foo.vars) end # If nothing is assigned to the '*rest' variable, then it is simply empty. # is the same as the current Ruby behavior. def test_keyword_no_splat @foo.grab(x:1, y:2) assert_equal [1,2,[]], @foo.vars end # The presence of a '*rest' declaration does not alter required arguments. def test_keyword_splat_with_not_enough assert_raises(ArgumentError) { @foo.grab(x:1) } end # Ensure that the presence of a parameter in one method declaration does # not interfere with an identically named parameter in another declaration. def test_no_local_leakage assert_nothing_raised { @foo.t2 x:8 } end # Ensure that "::" doesn't cause problems def test_works_with_constants assert_nothing_raised{ @foo.bar(y:1, x:2, z:Foo::MYCONST) } assert_nothing_raised{ @foo.bar(y: 1, x: 2, z: ::TOPLEVEL) } end def test_expected_argument_errors assert_raises(ArgumentError){ @foo.bar } # x & y missing assert_raises(ArgumentError){ @foo.bar(1) } # y missing assert_raises(ArgumentError){ @foo.bar(x:1) } # y missing assert_raises(ArgumentError){ @foo.bar(y:1) } # x missing assert_raises(ArgumentError){ @foo.bar(x:1, z:2) } # y missing assert_raises(ArgumentError){ @foo.bar([1,2,3]) } # y missing assert_raises(ArgumentError){ @foo.bar(*[1]) } # y missing assert_raises(ArgumentError){ @foo.bar(x:1,y:1,a:3) } # no 'a' parameter assert_raises(ArgumentError){ @foo.bar(*[1,2,3,4]) } # too many args end # Ensure that the subclass didn't pick up the named parameters # from the parent def test_expected_argument_errors_subclass assert_raises(ArgumentError){ @bar.bar } assert_raises(ArgumentError){ @bar.bar(x:1) } assert_raises(ArgumentError){ @bar.bar(y:1) } assert_raises(ArgumentError){ @bar.bar(z:1) } end def test_expected_syntax_errors assert_raises(SyntaxError){ @foo.bar(x:1, 2) } # positional must be first assert_raises(SyntaxError){ @foo.bar(*a, 1) } # splat must be last assert_raises(SyntaxError){ @foo.bar(*a, x:1) } # splat must be last end def teardown @foo = nil @arr = nil end end