goby
goby copied to clipboard
Discussion: Goby's parameter syntax in method definitions
TL;DR: I think we can simplify Goby's parameter syntax in method definition.
Of course the final decision should be @st0012's.
Terms
Before proceeding, let me define the followings to clarify the issues and to be concise:
- parameter: on method definitions
- argument: on method calling
Name as the following on Goby's current method parameters:
-
pN: normal parameters like
def foo(a)
- Unable to omit on method calling
- No type restrictions
-
pND: normal (optional) parameters with default value like
def foo(a=1)
- Can be omitted
- no type restrictions
-
pK: keyword parameters like
def foo(key:)
- Unable to omit on method calling
- No type restrictions
-
pKD: (optional) keyword parameters with default values like
def foo(key: "value")
- Can be omitted
- No type restriction
-
pVA: variable array parameters like
def foo(*array)
- Uses splat
*
for indicating pVA - Assumes an array will be taken
- Can be defined only once in a method definition
- Can be omitted (empty array
[]
will be passed when omitted) - No default value can be provided
- "Bare" arguments will be treated as elements of the array
- Uses splat
In Goby, the order of parameters are restricted as
- pN: zero or more
- pND: zero or more
- pK: zero or more
- pKD: zero or more
- pVA: zero or one
Note that the order of arguments for pK can be shuffled as far as they are grouped:
# Goby
def foo(key1:, key2:, key3:)
end
foo(key3: "foo", key2: "bar", key1: "baz")
This is the same for pKD:
# Goby
def foo(key1: "foo", key2: 99, key3: [])
end
foo(key3: [55, 44, 33], key2: 88, key1: "bar")
Ruby's parameters
In addition, Ruby has the following parameters:
-
pVH: variable hash parameters like
def foo(**hash)
- Uses double splat
**
for indicating pVH - Only hash can be taken(!)
- Can be defined only once in a method definition
- Can be omitted (empty hash
{}
will be passed when omitted) - No default value can be provided
- "Bare" keyword arguments will be treated as the elements of the hash
- Uses double splat
-
pB: block parameters like
def foo(&block)
- Uses ampersand
&
for indicating pVH - Assumes
Proc
object will be taken - Can be defined only once in a method definition
- No default value can be provided
- Uses ampersand
Ruby's current issues on method parameters/arguments
1. Ruby's keyword parameters/arguments are still not well-integrated
- Ruby's pND is now redundant because pKD covers the role and is preferable
- Ruby's pND like
option={}
is now obsolete and should be avoided
See the recent Rubocop rules that indicates keyword parameters with default values are preferable over parameters with default values with =
:
- https://github.com/rubocop-hq/ruby-style-guide/pull/509
- https://github.com/rubocop-hq/ruby-style-guide/commit/508e5062905831c318148b9002eb96f5a98b5524
2. Splat *
or double-splat **
on calling methods are often puzzling
Just to pass a variable that holds an array or a hash, we still need to add splats to the variables. I think this is also redundant.
# Ruby
def foo(*array, **hash)
p array
p hash
end
a = [99, 88, 77]
h = {key1: :value1, key2: :value2}
foo(*a, **h)
Just using pKD is sufficient:
# Ruby
def foo(array: [], hash: {})
p array
p hash
end
a = [99, 88, 77]
h = {key1: :value1, key2: :value2}
foo(array: a, hash: h)
3. Ruby is trying to handle keyword parameters as hash key-values, but sometimes fails:
This is one of the critical issues in current Ruby.
Ref: https://hackmd.io/8EMYfZ8KQwCbYrNogtIDIg
# Ruby
# Example1: assume the method exists first
def foo(*args)
args.each {|v| puts v.inspect }
end
foo([1, 2, 3]) #=> [1, 2, 3]
foo(key: 1) #=> {:key=>1}
# but just adding keyword arguments breaks the existing method calling!
def foo(*args, out: $stdout)
args.each {|v| out.puts v.inspect }
end
foo([1, 2, 3]) #=> [1, 2, 3]
foo(key: 1) #=> unknown key: k !!!
# Ruby
# Example2: assume the method exists first
def create_element(name, attrs={})
# do something
end
create_element("a", href: "URL") #=> works
# but just adding keyword arguments breaks the existing method calling!
def create_element(name, attrs={}, children: elements)
# do something
end
create_element("a", href: "URL") #=> unknown key: href !!!
Ruby comitters are trying to resolve the issue, but they recognizes that some breaking-changes are required.
4. Ruby's pVH with **
is in fact restricting the type (only hash can be taken)
(This is just what I discovered and perhaps not an issue :-)
Propositions to improve Goby's parameters
Considering above, Goby is evolving in good way, so I'd propose the followings:
1. Remove pND from Goby
As described above, pND, optional parameters with default value with =
, is redundant and can be removed from Goby.
Removing pND, we can still provide optional keyword parameters pKD in Goby (and Ruby as well).
2. Remove pVA *
from Goby
I think pVA, variable-length array parameters with splat *
can be removed as well.
Removing pVA, we can still provide variable-length arguments with array literal []
or hash literal {}
as well as the ones in variables:
# Goby
def foo(array: [], hash: {})
p array
p hash
end
foo(array: [99, 88, 77], hash: {key1: :value1, key2: :value2})
a = [99, 88, 77]
h = {key1: :value1, key2: :value2}
foo(array: a, hash: h)
This makes any splat operators unnecessary.
Final parameter syntax
So Goby's parameter syntax can be simple and concise:
- pN (zero or more)
- pK (zero or more)
- pKD (zero or more)
Of course the order of pNs/pKs/pKDs should be kept.
Well, now I feel that adding block parameters pB (
&block
) might be good for Goby. Correction: I should've remembered Goby already implementsget_block
😅 .
Advantages
- Splat operators
*
and**
are unnecessary - Reducing confusions regarding parameters and argumets
- Especially in "bare" variable-length params such as
1, 2, 3...
orhash1: "value1", hash2:, "value2:...
- We can now distinguish keyword params/args and hash's key-value
- Especially in "bare" variable-length params such as
I recognize that keyword-params/args are different from hash's key-value pairs.
This makes us avoid confusions when adding parameters in the future.
new Sample
In other words, you should always use pK or pKD to pass variable-length arguments. I hope this does not annoy developers so much.
# Goby
def form_with(model: nil, scope: nil, url: nil, format: nil, opt: {})
...
end
form_with opt: {skip_enforcing_utf8: true}, model: Post.first do |form|
form.text_field :title
end
# Goby
def camelize(term, uppercase_first_letter: true)
...
end
camelize('active_model')
camelize('active_model', uppercase_first_letter: false)
# Goby
class KeyGenerator
def initialize(secret, opt: {})
@secret = secret
@iterations = opt[:iterations] || 2**16
end
end
KeyGenerator.new("secretkey")
KeyGenerator.new("secretkey", opt:{iteration: 2**16})
3. Experimental: type-checking with []
or {}
This is just an experimental idea. If []
or {}
are specified with pKD, the type (Array
or Hash
) will be restricted as that.
I believe this does not break duck-typings.
# Goby
def foo(array1: [] array2: [1, 2, 3], hash1: {}, hash2: {key1: :value1})
puts array1
puts array2
puts hash1
puts hash2
end
foo(array1: 1) # TypeError
foo(hash2: [1, 2, 3]) # TypeError
I look forward to your comments.
@hachi8833 I'm just replying some of the suggestion at a time
Remove pND from Goby
I think this is worth trying, can't come up with any drawback at this point. Removing it does reduce some edge cases and can simplify our argument checking logic.
Remove pVA * from Goby
I don't agree with this so much. I think more times we use pVA is because we want to pass arguments more dynamically, making users pass an array to keyword argument doesn't seem to be a solid solution. (I can't think about any specific example right now, will update this comment if I got any). Also, this is a very common syntax among most of the popular languages. The new way might not be that straightforward for new users.
Experimental: type-checking with [] or {}
This is interesting and makes sense. I'd love to try this.
Anyway, we'll need to first decide if we're going to keep pND. And then we need to fix https://github.com/goby-lang/goby/issues/497 before we move forward. Also, I'm very appreciated for your help on this 😄
Thank you for the reply!
I notice that pVA in Goby is always follows other parameters and this syntax looks sufficient. I'm OK to preserve pVA 😃 .
FYI: The following behavior has been prohibited in Ruby 2.6:
def foo(h = {}, key: :default)
p [h, key]
end
foo(:key => 1, "str" => 2)
#=> [{"str"=>2}, 1] in 2.5
#=> non-symbol key in keyword arguments: "str" (ArgumentError) in 2.6
@hachi8833 can you help me open a PR and add some tests for testing removing pND
? I think I can try to implement it
I'd try this
Just wait for the tests for removing pND
some more 💦
FYI: https://bugs.ruby-lang.org/issues/14183 (still under discussion) Ruby committers are trying to change the behaviors around this like that:
def foo(**kw); p kw; end
def bar(kw = {}); p kw; end
h = {:k => 1}
# base (non-braced) hash arguments passed as keywords
foo(k: 1) #=> {:k=>1} in 2.X and 3.0
foo(:k => 1) #=> {:k=>1} in 2.X and 3.0
foo(**h) #=> {:k=>1} in 2.X and 3.0
bar(k: 1) #=> {:k=>1} in 2.X, ArgumentError in 3.0
bar(:k => 1) #=> {:k=>1} in 2.X, ArgumentError in 3.0
bar(**h) #=> {:k=>1} in 2.X, ArgumentError in 3.0
# braced hash arguments are passed as a last argument
foo({ k: 1 }) #=> {:k=>1} in 2.X, ArgumentError in 3.0
foo({ :k => 1 }) #=> {:k=>1} in 2.X, ArgumentError in 3.0
foo(h) #=> {:k=>1} in 2.X, ArgumentError in 3.0
bar({ k: 1 }) #=> {:k=>1} in 2.X and 3.0
bar({ :k => 1 }) #=> {:k=>1} in 2.X and 3.0
bar(h) #=> {:k=>1} in 2.X and 3.0
@hachi8833 I'll probably go for Ruby 3.0's definition except **
FYI: Ruby finally chose to separate keyword args from positional args: https://github.com/ruby/ruby/pull/2395 Looks they continue to work with it to resolve issues on delegation with args.
The following is the latest and the most comprehensive document for breaking changes in Ruby 2.7~:
https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/