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

Compiler: implement double splat #2580

Merged
merged 1 commit into from
May 12, 2016
Merged

Compiler: implement double splat #2580

merged 1 commit into from
May 12, 2016

Conversation

asterite
Copy link
Member

@asterite asterite commented May 11, 2016

This PR adds double splatting to the language.

A double splat:

  • can appear in a method/macro definition and in a call
  • when used in a call argument, it "splats" a named tuple
  • when used in a method argument, it "unsplats" named arguments

If you are familiar with Ruby double splat, it's similar, although a bit different in Crystal because Crystal doesn't have keyword args, and we also use a named tuple instead of a hash.

With some examples all of this will become clear, you can look at the specs here and here.

With this, we can make the delegate macro work with named arguments, because this now works:

def foo(x, y)
  x - y
end

# forward all args and named args
def bar(*args, **nargs)
  foo(*args, **nargs)
end

bar 10, 2 # => 8
bar 10, y: 2 #=> 8
bar x: 10, y: 2 # => 8
bar y: 2, x: 10 # => 8

Another useful thing is that one can now catch all named arguments with a double splat:

def foo(**options)
  options[:x] - options[:y] 
end

foo y: 2, x: 10 # => 8

The above is specially useful to avoid repeating a set of options in multiple methods that simply forward this information. Remember that doing options[:x] in the code above will give a compile error if x is not passed, so it's super type-safe.

With the above, we can also force method arguments to always be passed as named arguments. For example:

# options must include :element and :in
def put(**options)
  options[:in] << options[:element]
end

ary = [1, 2, 3]
put element: 4, in: ary
ary # => [1, 2, 3, 4]

puts 4, [1] # compile error
put element: 4, innnn: [1, 2, 3] # compile error (when accessing options[:in])

The only "problem" with the above is that we can pass extra named arguments and we won't get an error. We can solve this problem later with a type restriction on options, to restrict the keys and also the types.

Double splats are also available in macro arguments as well.

In a method/macro, a double splat can be mixed with a splat:

def foo(*args, **options)
end

foo 1, 2, x: 10, y: 20 # OK, args is {1, 2}, options is {x: 10, y: 20}

However, this doesn't work if there are positional arguments in the method/macro definition:

def foo(x, *args, **options)
end

foo 10, 20, 30, y: 40, z: 50 # probably obvious
foo 10, 20, 30, x: 40, z: 50 # but this...?

The reason is that it becomes very hard to know what would happen, where the args will go, and it might also not be very useful, so for now it's out of the language.

Of course after this the only thing remaining to be able to entirely forward a call's information is forwarding a block. This will probably come in the future, right now it's kind of hard to do.

@oprypin
Copy link
Member

oprypin commented May 11, 2016

What exactly is args in the "forward all args" example and how can it work?

Why not pass only positional args with * and only named arguments with ** ?

@oprypin
Copy link
Member

oprypin commented May 11, 2016

PEP 3102 -- Keyword-Only Arguments might be an interesting read since you're mentioning this idea. Because otherwise this is probably more similar to Python than Ruby at this point.

@asterite asterite force-pushed the feature/double_splat branch from e4e662e to 236e697 Compare May 12, 2016 11:57
@asterite
Copy link
Member Author

asterite commented May 12, 2016

@BlaXpirit args would be a tuple with all the positional arguments and also, in the last position, a named tuple with the named args. But it felt like a hack, and we chose to do that mostly because Ruby kind of does that, but when I try to explain this it's confusing and feels like magic, so I decided to remove this. Now you have to be explicit: I updated the original PR description with this.

That said, I really (I mean, really ❤️) like that PEP you mention. I think it's perfect, because the logic is super simple (both for a human and a compiler), and it's also a way to force arguments to be passed as named arguments. And it also open the possibility to overload based on these required named arguments:

# Warning, silly example follows
def print(array : Array)
end

def print(array : Array, *, without)
  print array.delete(element)
end

def print(array : Array, *, and_also)
  print array
  print and_also
end

print [1, 2, 3] # calls the first overload
print [1, 2, 3], without: 2 # calls the second overload
print [1, 2, 3], and_also: 4, # calls the third overload

This for example is possible in Swift. And because arguments after the * necessarily have to be passed as named arguments, they can be part of a method's signature. I want to implement this 😸 . I'll check with @waj, but I'm sure he'll like it too (he likes this in Swift and wants this in Crystal).

The only "downside" (not a downside for me, though), it that right now it's possible to have this in Crystal (and, well, Ruby):

def foo(x, *args, y)
end

foo 1, 2, 3, 4 # x is 1, args is {2, 3}, y is 4

However, I never found a useful use case for that. And in any case, I think invoking it like this is much clearer:

foo 1, 2, 3, y: 4

One clear example of this is the current delegate macro. Right now it's like this:

# One required positional arguments, other optional arguments, and the final argument
# is the one we'll delegate the method to
macro delegate(method, *other_methods, to_object)
end

# delegate foo and bar to baz
delegate foo, bar, baz

# Can't invoke it like this, because right now it's confusing
delegate foo, bar, to_object: baz

If apply the PEP we can write it like this:

macro delegate(*methods, to)
end

delegate foo, to: bar
delegate foo, bar, to: baz

I mean, the definition is shorter and easier to implement, and at the caller side we are forcing a nice, readable syntax. It's a total win.

@asterite
Copy link
Member Author

If we decide to go that way (following the last comment), I'll probably merge this first because it has the logic for the double splat, and then update the matching logic accordingly.

@asterite asterite merged commit 84a8c10 into master May 12, 2016
@ozra
Copy link
Contributor

ozra commented May 12, 2016

Seems pretty clean, the PEP-definition, I can definitely live without post-splat "positional slots".

@asterite
Copy link
Member Author

@BlaXpirit @ozra I'll try to implement it and then send a PR

@asterite asterite deleted the feature/double_splat branch May 13, 2016 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants