goby-lang / goby

Goby - Yet another programming language written in Go
MIT License
3.49k stars 171 forks source link

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

Open hachi8833 opened 5 years ago

hachi8833 commented 5 years ago

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:

Name as the following on Goby's current method parameters:

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:

Ruby's current issues on method parameters/arguments

1. Ruby's keyword parameters/arguments are still not well-integrated

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

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 commented 5 years ago

@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 😄

hachi8833 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

I'd try this

hachi8833 commented 5 years ago

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 commented 5 years ago

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

hachi8833 commented 4 years ago

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.

hachi8833 commented 4 years ago

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/