-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add Slice#+(Slice)
and Slice.join
#12081
Add Slice#+(Slice)
and Slice.join
#12081
Conversation
# ``` | ||
# | ||
# See also: `#+(other : Slice)`. | ||
def self.join(slices : Indexable(Slice)) : Slice |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 I think we should also have an overload that accepts a splat. That way you can do Slice.join(slice1, slice2, slice3)
which I think is going to be the most common scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is a splat overload then Slice(Slice)
becomes ambiguous:
def Slice.join(slices : Indexable(Slice)); end
def Slice.join(*slices : Slice); end
# first overload
Slice.join(Slice[Slice[1], Slice[2]]) # => Slice[1, 2]
Slice.join(Slice[Slice[1]]) # => Slice[1]
# second overload
Slice.join(Slice[Slice[1]], Slice[Slice[2]]) # => Slice[Slice[1], Slice[2]]
Slice.join(Slice[Slice[1]]) # => Slice[Slice[1]]
Neither overload is more specific than the other, so Slice.join(Slice[Slice[1]])
's return value depends on the definition order.
Maybe we can require the splat overload to take 2+ arguments, i.e. (slice : Slice, *slices : Slices)
? Note that if someone has a Tuple
of Slice
s already then they don't need to explode the Tuple
.
(Actually this reasoning applies to some other methods too, such as Object#in?
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither overload is more specific than the other
Is that the case?
I just tried this:
def foo(x)
puts "no splat"
end
def foo(*x)
puts "yes splat"
end
foo(1)
foo(1, 2)
It prints "no splat" and "yes splat". Then I tried this:
def foo(*x)
puts "yes splat"
end
def foo(x)
puts "no splat"
end
foo(1)
foo(1, 2)
Same output.
So a no-splat overload is more specific than a splat overload. I don't think there's any ambiguity, because if you do Slice.join(...)
you would only want the splat version when there's more than one element in that list. Which... I guess means we could also make it explicit with (slice, *slices)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If neither x
parameter has a restriction, then yes, the splat-less overload comes first. In fact, the restrictions given above also make the splat-less overload come first, for now:
def foo(slices : Indexable(Slice)); "indexable"; end
def foo(*slices : Slice); "splat"; end
def bar(*slices : Slice); "splat"; end
def bar(slices : Indexable(Slice)); "indexable"; end
slice = Slice[Slice.new(1) { 1 }]
foo(slice) # => "indexable"
bar(slice) # => "indexable"
But if #10711 is merged and -Dpreview_overload_order
is defined, then:
- The two
slices
parameters are corresponding parameters, because they both match the first positional argument in any valid call; Indexable(Slice) < Slice
is false;Slice < Indexable(Slice)
is also false;- Therefore, neither overload is stricter than the other, because at this point the splat has no effect.
Thus bar(slice)
will output "splat"
in the future. This is why I believe the splat overload should take 2+ parameters so that the two overloads become disjoint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 2+ parameter solution would lead to surprising results when the number of arguments comes from indirection (e.g. a macro or splat). The behaviour would change significantly when there is only a single argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a splat is unnecessary because the Indexable
overload takes care of Tuple
arguments already. Macros are probably better off wrapping all arguments to Slice.join
and similar methods within a Tuple.new
call to make the intent explicit:
macro bad(*x)
::Slice.join({{ x.splat }})
end
macro good(*x)
::Slice.join(::Tuple.new({{ x.splat }}))
end
The 2+ requirement signals the fact that the splat overload is user-facing, rather than generated code-facing. And then even in user-written code, switching from the splat overload to the Indexable
overload is a matter of adding a pair of {}
(and maybe ()
because the syntax requires it) around the arguments; the performance is the same whether the Tuple
formation is done by the language or the user. I think we should eventually clean up splat overloads like those in the standard library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handling for read_only
is missing. I think as soon as one of the slices is read_only
, the resulting slice should be read_only
as well.
The new |
Hm, if |
I could see an argument that |
{% elsif T < Slice && @type < Indexable %} | ||
# optimize for slice | ||
Slice.join(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this is really surprising behavior for me, but given that it is done for arrays I guess it make sense. Where does support for Array come from? The optimization comes from c2c4d5d but I do wonder if it was intentional that it worked as a synonym of flatten(1) in the first place. (it gives a typeerror in ruby, for reference)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default argument of Ruby's Enumerable#sum
is 0
; Ruby does not have the notion of .additive_identity
or try to infer it. This works:
[[1, 2], [3, 4]].sum([]) # => [1, 2, 3, 4]
only that there is probably no optimization here.
@straight-shoota That seems to be true. The only use case I recall seeing for this is to support the contract of strings being immutable, even if you end up trying to do things like I do like @yxhuvud's suggestion of letting the caller of |
Just realized we can apply the same treatment to other non-resizable |
I'm not entirely sure what's left to do here. Adding Similar implementations for other types should better be handled on their own. |
I did not add a In both cases, adding the feature later wouldn't constitute a breaking change. |
Resolves #10157.