From: Brian Mitchell Date: 2005-10-22T03:08:19+09:00 Subject: Re: A comparison by example of keyword argument styles Hello again, I am updating my original post with more information, namely some corrections for s_ and some new discussion at the bottom. I am will say again that the stuff bellow probably still contains errors but I hope that it is at least more accurate than my original. I left all the original text there so we could have a nice comparison of now and then. It also serves as a good example of how something might work for an implementation that differs in a way that is congruent to my original post. On 10/21/05, 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_. > This was one part confusing me about s_. When we get our list it could either be: [1,2,3,4] or [{:k1 => 1, :k2 => 2, :k3 => 3}] The problem is still there but in variation, the arguments might or might not be in the hash. This blurring the line between the method and the caller, which are currently two very separated things. Checking this for an interface that handles not so simple calls might be a burden more than an assistance. I will keep my conclusion between the two. Another problem brought to my attention by Matz's post is delegation. One can not use this to delegate unless you keep you method interface the same, this could be problematic over long chains of argument passing (i.e. super could have unintended meaning now) To clarify m_'s behavior I will copy some examples from Matz's slides with some annotations: def baz(*rest, a:4, b:0, **keys) ... end baz() # rest=[], a=4, b=0, keys={} <- notice that [] in args has an implicit {} for **keys baz(1) # rest=[1], a=4, b=0, keys={} <- explicit keys do not count as positionals baz(a:1) # rest=[{a:1}], a=1, b=0, keys={a:1} <- * will always contain the full set of passed variables. baz(a:1, b:2) # rest=[{a:1, b:2}], a=1, b=2, keys={a:1, b:2} baz(1, 2, b:2) # rest=[1, 2, {b:2}], a=4, b=2, keys={b:2} <- interesting. This result is for passing on correct values for delegation. baz(c:2) # rest=[{c:2}], a=4, b=0, keys={c:2} <- another one to think about. > 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. > I will extend this with another way to explain it. *args means you can also pass any number of keys also. This goes for both. This means there is no way of having only variable numbers of positional arguments and static numbers of keyword arguments. m_f4(1,2,3, a:4, b:5, c:6) <- example of what I mean. > 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. > I was wrong on this one. Let me try again. For s_ you get: [1,2, { :a => 3, :b => 4}] if I understand correctly. m_ gives you [1,2, {:a => 3, :b => 4}]. They seem to yield the same result but let me give a new the s_ example to show how they are practically the same: def s_f6(a, b, *args) s_f6(0, 1, 3, b:2) s_f6's *args would yield something like [3, {:b => 2}] in this case (If I get Daniel's reply correctly). a = 0, b = 1 s_f6(1, 3, a:0, b:2) This time *args would look like [1, 3, {:b => 2}]. a = 0, b = 1. This is an example of where it doesn't break which is in contrast to my original interpretation. It seems neither would win here after further analysis. It is a matter of style for this example. > 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 > This method is much cleaner now: > def self.s_create(kind, name, *args) > pet = Pet.allocate > pet.kind = kind > pet.name = name get = args.last > 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. > I've updated quite a bit of the code and commentary to reflect things. The last comparison is mostly equal however, it does show some ambiguities with calling in the future for both methods: def foo(*args) ... end foo(1, 2, a:3, b:4) foo(1, 2, {:a => 3, :b => 4}) *args gets the same array in both cases. Now before Daniel goes off on me about behaviors and how it handles things ;) ... lets me continue with more evolution happening in the community: Behaviors allow Sydney to implement a prototype to these types of behaviors. However, since they are a separate change to the language I will leave it to someone else to give an overview of what they do and don't give use. The interesting part is where Evan is taking his implementation. Currently, using a basic array for *args can cause ambiguous calls. This could be solved by attaching an empty hash at the end of args in all cases or maybe only certain cases (though something more dependable is likely to be less work for the human mind). This make it feel like we are continuously trying to fix the wrong approach to keyword arguments (I speak for myself only). After speaking with Evan a little about his plans in more detail, he discussed that he is considering using a new kind of object in place of an Array for *. The new class (which I will call Arguments), would still act like an array (* would still work for expansion into delegated calls). However, one could now go: def bar(*rest) rest.keywords ... etc ... # I am sure we could come up with a suitable interface. end This is the exact kind of thing I thing we should be looking for. I don't know how Matz would feel about it. In fact I still am not sure myself as I haven't tried using it in examples yet. The point stands that there might be a good compromise to make between s_ and m_. This goes a long ways towards that IMO. Evan says he will be working on an implementation of this for Sydney. I look forward to testing it live rather than typing code into GMail. Matz, are there any patches that implement your proposal yet? I would love to test that out too. If not anyone volunteer? I know I would be wasting my time trying as I am not that much of an internals wizard. Thanks, Brian.