Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Goby's parameter syntax in method definitions #751

Open
hachi8833 opened this issue Oct 31, 2018 · 9 comments
Open

Discussion: Goby's parameter syntax in method definitions #751

hachi8833 opened this issue Oct 31, 2018 · 9 comments

Comments

@hachi8833
Copy link
Member

hachi8833 commented Oct 31, 2018

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

In Goby, the order of parameters are restricted as

  1. pN: zero or more
  2. pND: zero or more
  3. pK: zero or more
  4. pKD: zero or more
  5. 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
  • 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

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 =:

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:

  1. pN (zero or more)
  2. pK (zero or more)
  3. 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 implements get_block 😅 .

Advantages

  • Splat operators * and ** are unnecessary
  • Reducing confusions regarding parameters and argumets
    • Especially in "bare" variable-length params such as 1, 2, 3... or hash1: "value1", hash2:, "value2:...
    • We can now distinguish keyword params/args and hash's key-value

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.

@st0012
Copy link
Member

st0012 commented Nov 2, 2018

@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 #497 before we move forward. Also, I'm very appreciated for your help on this 😄

@hachi8833
Copy link
Member Author

hachi8833 commented Nov 2, 2018

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 😃 .

@hachi8833
Copy link
Member Author

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

@st0012
Copy link
Member

st0012 commented Feb 3, 2019

@hachi8833 can you help me open a PR and add some tests for testing removing pND? I think I can try to implement it

@hachi8833
Copy link
Member Author

I'd try this

@hachi8833
Copy link
Member Author

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

@st0012
Copy link
Member

st0012 commented Mar 16, 2019

@hachi8833 I'll probably go for Ruby 3.0's definition except **

@hachi8833
Copy link
Member Author

FYI: Ruby finally chose to separate keyword args from positional args:
ruby/ruby#2395
Looks they continue to work with it to resolve issues on delegation with args.

@hachi8833
Copy link
Member Author

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants