Brian Mitchell wrote:
> Hello fellow rubyists,
>
> What I have bellow is what started as a post to RedHanded. It was
> growing in size too rapidly so I decided to post here for all to see.
> Sorry for starting yet another thread on these topics. It is rough so
> please don't nit pick details. I don't want this to start a flame war
> (though I can't do much about that now). I would rather see some ideas
> on how to get the best of both worlds. Some of this won't come without
> a compromise so keep that in mind. I apologize in advance if I did
> make any grievous errors in my interpretations.
>
> There is a matter of taste involved but beyond that there are a few
> easy comparisons. I will try to keep this down to just that (though a
> few may be on a grey line, I hope they are clear enough).
>
> Let me cite Matz's slides first:
> * Make method calls more descriptive
> * Order free arguments
>
> With that simple goal in mind, lets start the comparisons.
>
> Sydney's argument scheme (s_ henceforth) is simple when you want to
> reorder the arguments.
>
> def s_f1(a,b,c) ... end
> s_f1(b:2, a:1, c:3)
>
> Matz's scheme (m_ from now on) allows this too:
>
> def m_f1(a:,b:,c:) ... end
> m_f1(b:2, a:1, c:3)
>
> Ok. Not much difference right away no clear winner. Lets examine
> another variation on calling these:
>
> s_f1(1,2,3) # Simple. current behavior.
> m_f1(1,2,3) # Error. No positional arguments.
>
> This shows one point that goes to s_. It makes it easy to use keyword
> args that still have position. However, Matz could consider to allow
> keyword arguments to count as positional arguments and make this
> example go away. It is up to him. +1 for s_ for now. The change would
> force non keyword args to come before keyword args. simple enough.
> Though I still don't see a good reason to share both positional and
> keyword arguments (a good example would be of great help in this
> discussion).
>
> The next example will be a method that takes any number of key-worded
> arguments:
>
> def m_f2(**keys) ... end
> m_f2(k1: 1, k2: 2, k3: 3)
>
> def s_f2(*args) ... end
> s_f2(k1: 1, k2: 2, k3: 2)
>
> That works but there are some complications that the s_ method starts
> to see (the hidden ugly head). *args now gets an array with hash
> (key-value) or a value with each entry. Ugly. Now something internal
> is depending on how someone on the outside (away from the interface)
> called it to see what it gets. I hope this is clear enough for you. +1
> for m_.
>
> How about mixing positional and keyword args?
>
> def m_f3(p1, p2, k1:, k2:) ... end
> def s_f3(p1, p2, k1, k2) ... end
>
> *_f3(1,2,k1:3, k2: 4)
>
> Not much difference. m_ requires the extra : to be added. This is
> neither a plus or a minus as it can be easily argued both ways. No
> winner. (I will argue it if needed but trust me one can look both
> ways).
>
> How about having a variable number of positional arguments and a set
> number of keys?
>
> def m_f4(*args, a:, b:)
> m_f4(1,2,3,4,5,6,7,8, a:1, b:2) # misleading see bellow.
>
> def s_f4(a, b, *args)
> s_f4(1,2,3,4,5,6,7,8, a:1, b:2) # might have the same problem
>
> The s_ example is nice. It show an intuitive behavior at first but
> depending on implementation you can no longer pull a variable number
> of key paris or you have the same semantic problem that the m_ one
> has. If you use * at all that allows any number of arguments of any
> type to be passed. Assuming the latter behavior (needed for *args to
> work with delegation), then neither has any gain. I may be miss
> understanding s_ at this point so please point it out.
>
> How about having both keyword and positional arguments mixed in a
> catch-all with *?
>
> def m_f5(*args)
> m_f5(1,2, a:3, b:4)
>
> def s_f5(*args)
> s_f5(1,2 a:3, b:4)
>
> Well things start to contrast now. For s_ you get: [1,2, { :a => 3}, {
>> b => 4}] if I understand correctly. m_ gives you [1,2, {:a => 3, :b
> => 4}]. I won't debate on which is better in this case. Most of this
> is involved with opinion. However, if you want to look at positionals
> alone and keys alone it is easy with m_ we now have the hash collected
> at the end of *args and can use **keys if we want to. Not a huge plus
> but a point to make things easy. It will minimize boilerplate code on
> a method. I give m_ a +1, you may disregard it if you don't agree.
>
> Now think about the above before we move on. Keep in mind that it is
> not just another way to call a method but gives the method's interface
> a richer meaning (like Matz's slide said).
>
> Now for some more concrete examples of usage:
>
> Say we have an object that we create from a class through some special
> method. The some arguments are required while others may or may not be
> there but the circumstances differ. Imagine that the list of
> attributes that can be passed may become quite long so using default
> arguments wouldn't be a very good idea. Or even further, the keys
> might be passed to a second function. This would normally be odd code
> to see but it shows how the nature of the two methods differ by quite
> a bit in real use.
>
> # Untested code. Could contain errors. At least I have an excuse this
> time. class Pet
>   def self.m1_create(kind, name, **keys)
>     pet = Pet.allocate
>     pet.kind = kind
>     pet.name = name
>     case(kind)
>     when :ham
>       pet.weight = keys[:weight]
>     when :cat
>       pet.color = keys[:color]
>     when :dog
>       pet.color = keys[:color]
>       pet.breed = keys[:breed]
>     when :ruby
>       pet.facets = keys[:facets]
>     else
>       fail "Uknown kind of pet: #{kind}"
>     end
>   end
>
>   # Same as m1_ but with a different method argument style.
>   def self.m2_create(kind:, name:, **keys)
>     # Lazy me ;) They are the same otherwise anyway.
>     m1_create(kind,name,**keys)
>   end
>
>   def self.s_create(kind, name, *args)
>     pet = Pet.allocate
>     pet.kind = kind
>     pet.name = name
>     # Messy solution. There is probably a better one.
>     get = lambda {|sym|
>       args.find(lambda{{}}) {|e|
>         e.kind_of? Hash && e[sym]
>       }[sym]
>     }
>     case(kind)
>     when :ham
>       pet.weight = get[:weight]
>     when :cat
>       pet.color = get[:color]
>     when :dog
>       pet.color = get[:color]
>       pet.breed = get[:breed]
>     when :ruby
>       pet.facets = get[:facets]
>     else
>       fail "Uknown kind of pet: #{kind}"
>     end
>   end
> end
>
> Pet.m1_create(:ham, "selfish_ham", weight:2.3)
> Pet.m2_create(kind: :cat, name: "cat43", color: :black)
> Pet.s_create(:dog, "singleton", color: :brown, breed: :mini_pincher)
> Pet.s_create(kind: :ruby, name: "JRuby", facets: 26)
>
> My s_ method is messy and could probably be cleaned up but it still
> serves a point. Savor the style for a bit. It might add more verbosity
> but I think it gives us some good side effects for the small price
> (IMHO again). I think some really good points can be made for both
> side but my _feeling_ is that Ruby doesn't need another halfway there
> feature (IMHO). Keyword arguments are serious things and should be
> treated as part of your interface (IMHO). I feel that the semantics of
> m_ are more clear than the at first simpler look of s_ (IMHO -- why
> not just automatically append these till the end of my message). It is
> a hard choice. We still have one more option that I know of, change
> nothing. Hashes seem to get the job done for most people already. I
> know I missed something so please add to this. If I made any errors
> please correct them. Just avoid and unproductive and personal attacks
> please.
>
> Thanks for reading this far,
> Brian.

Wow!  Quite a comprehensive discussion.  Thanks for writing this far! :-)

I think I have to digest this a bit, here are some first reading thoughts:

 - Example for mixing keyword and positional args: like you showed, pos
args for mandatory, keyword args for multiple optional values.

 - I guess Sydney's argument scheme will cause serious performance
drawbacks if there are multiple keyword arguments because there is one
hash per pair.

 - Mixing both styles for a single method feels not right in Ruby because
it seems more complicated than necessary (just look at the length of the
part of your discussion that deals with this).  Stored procedures of RDBMS
can usually be called with parameters given either positional style *or*
keyword style (btw, without a difference in declaration) but not mixed.  I
would only allow one of the two approaches.

 - Personally I'd probably favour an approach which allows only one
calling style at a time.  Variables that have no default values declared
will just be nil.  Basically this would be the equivalent of this with
nicer syntax:

old:
def foo(h)
  h[:length] * h[:width]
end

foo(:length => 10, :width => 20)

new:
def foo(length, width)
  length * width
end
foo 10, 20
foo width:20, length:10

Disclaimer: these remarks might not be too well thought out.

Kind regards

    robert