B
Brian Mitchell
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 argum=
ents:
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 =3D> 3}, {
:b =3D> 4}] if I understand correctly. m_ gives you [1,2, {:a =3D> 3, :b
=3D> 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 =3D Pet.allocate
pet.kind =3D kind
pet.name =3D name
case(kind)
when :ham
pet.weight =3D keys[:weight]
when :cat
pet.color =3D keys[:color]
when :dog
pet.color =3D keys[:color]
pet.breed =3D keys[:breed]
when :ruby
pet.facets =3D 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 =3D Pet.allocate
pet.kind =3D kind
pet.name =3D name
# Messy solution. There is probably a better one.
get =3D lambda {|sym|
args.find(lambda{{}}) {|e|
e.kind_of? Hash && e[sym]
}[sym]
}
case(kind)
when :ham
pet.weight =3D get[:weight]
when :cat
pet.color =3D get[:color]
when :dog
pet.color =3D get[:color]
pet.breed =3D get[:breed]
when :ruby
pet.facets =3D get[:facets]
else
fail "Uknown kind of pet: #{kind}"
end
end
end
Pet.m1_createham, "selfish_ham", weight:2.3)
Pet.m2_create(kind: :cat, name: "cat43", color: :black)
Pet.s_createdog, "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.
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 argum=
ents:
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 =3D> 3}, {
:b =3D> 4}] if I understand correctly. m_ gives you [1,2, {:a =3D> 3, :b
=3D> 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 =3D Pet.allocate
pet.kind =3D kind
pet.name =3D name
case(kind)
when :ham
pet.weight =3D keys[:weight]
when :cat
pet.color =3D keys[:color]
when :dog
pet.color =3D keys[:color]
pet.breed =3D keys[:breed]
when :ruby
pet.facets =3D 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 =3D Pet.allocate
pet.kind =3D kind
pet.name =3D name
# Messy solution. There is probably a better one.
get =3D lambda {|sym|
args.find(lambda{{}}) {|e|
e.kind_of? Hash && e[sym]
}[sym]
}
case(kind)
when :ham
pet.weight =3D get[:weight]
when :cat
pet.color =3D get[:color]
when :dog
pet.color =3D get[:color]
pet.breed =3D get[:breed]
when :ruby
pet.facets =3D get[:facets]
else
fail "Uknown kind of pet: #{kind}"
end
end
end
Pet.m1_createham, "selfish_ham", weight:2.3)
Pet.m2_create(kind: :cat, name: "cat43", color: :black)
Pet.s_createdog, "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.