From: Brian Mitchell Date: 2005-10-21T15:47:52+09:00 Subject: A comparison by example of keyword argument styles 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.