-
-
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
Parse empty array/hash like literals #10192
base: master
Are you sure you want to change the base?
Parse empty array/hash like literals #10192
Conversation
I think we should only allow this for generic types or for aliases. |
Why? Using non-generic types is completely valid. For example |
But in that case you can use new. I don't think the original issue is an actual issue. |
Sure, you can use |
Theres no need to add special logic in a macro. Just use new and append the elements. The end result is the same. I just find it strange to allow String{} and such. |
Wait, wasn't |
I don't understand the intent behind the original issue; if you omit |
@Sija Yes, that's the intention. @HertzDevil A generic type without type argument won't work of course. This patch makes sure to raise an appropriate error in that case. But the describes use case applies for non-generic types and generic instance types. I've added a check that makes sure the instance type responds to |
I've been thinking about this. Maybe the best way to handle this is to expand these literals into a T.literal(...) macro. It accepts a single argument, a list, for array-like types, or two arguments, two lists, for Hash-like types. The nice thing about that is that the source code for the expansión is in Crystal, and it can be redefined. Thoughts? |
And a third overload without arguments for the empty list case. |
Yes, a standardized API similar to |
We should probably still merge this. At least the parts that fix bugs and add specs for existing behaviour. |
I'm fine with merging this as is. If we want to implement later T.literal it can work. I am afraid if the overloads might not explode or reach a limit in the number of arguments. Yet I find it weird to write @asterite are you against merging this to allow the empty case? |
I guess it's fine. With this you can do: class Person
end
person = Person {} Maybe it doesn't matter too much 🙃 |
I personally wasn't aware you could have spaces between the |
There's little or maybe no precedent for significant whitespace at a word boundary. Might even be very difficult to ensure in the parser. |
The only place this happens is named argument vs type declaration. But I don't mind the space here. |
I've stumbled across this again and continue to wonder why nobody has approved this yet... This change just makes sure that array- and hash-like literals work consistently for any number of elements (including zero). |
node.name = generic | ||
when GenericClassInstanceType | ||
if node.elements.empty? && !(type.has_def?("<<") || type.has_def?("[]=")) | ||
node.raise "Type #{type} does not support array-like or hash-like literal" |
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.
💭 Optional! The error could be a bit more explicit, like saying:
does not support array-like literal (it has no
<<
method) or hash-like literal (it has no[]=
method)
It is worth mentioning that this is now possible: class Foo(*T); end
Foo{1} # => #<Foo(Int32):0x7f913e9c1e90>
Foo{} # => #<Foo():0x7f913e9c1ea0> even though |
Looking at the specs, this PR indeed changes the meaning of class Foo(*T)
def <<(x); end
end
macro f(x)
# *x* is an `ArrayLiteral`, which has `#splat`
Foo{ {{ x.splat }} }
end then |
@HertzDevil That means |
If |
It's only really ambiguous if a type has a splat type parameter and implements both |
Then the following will silently become a macro f(x)
# *x* is a `HashLiteral` or `NamedTupleLiteral`
Foo{ {{ x.double_splat }} }
end It is very much worth worrying about because it is still enabling new behaviour, and reverting that always constitutes a breaking change, however unlikely to happen. By the way, what would happen in this case? class Foo(T)
macro method_missing(call)
end
end
Foo(Int32){} |
We can easily tell apart empty array-like and empty hash-like literals for generic types based on the arity of its type parameters. 1 type param = array-like, 2 type params = hash-like. It's only ambiguous with a splat parameter. And I don't think that's a likely use case for a collection type that can be used with such a literal.
I've started refactoring the implementation and I'll post an update later. |
Since the original issue only asks for a class Foo1(T); end
class Foo2(T, U); end
class Foo3(T, U, V); end
class Foo4(*T); end
class Foo5(T, *U); end
class Foo6(T, *U, V); end
# generic, unambiguously array-like
typeof(Foo1{}) # => Foo1(NoReturn)
# generic, unambiguously hash-like
typeof(Foo2{}) # => Foo2(NoReturn, NoReturn)
# generic, cannot be instantiated with 1 or 2 type arguments
typeof(Foo3{}) # Error: Foo3(T, U, V) supports neither array-like nor hash-like literals (wrong number of type vars)
# generic, can be instantiated with either 1 or 2 type arguments
typeof(Foo4{}) # Error: cannot infer Foo4(*T)'s type variables (ambiguous array-like or hash-like literal)
typeof(Foo5{}) # Error: cannot infer Foo5(T, *U)'s type variables (ambiguous array-like or hash-like literal)
# generic, unambiguously hash-like
typeof(Foo6{}) # => Foo6(NoReturn, NoReturn) And then |
I don't think it's true that one type argument is array. For example you could have a Hash like type with always string keys, but the values are generic. Given all the complexity this little change involves, and how it doesn't add anything new to the table, I think we should simply drop this. You can use new for this. |
Yes, that's entirely possible. But such a type can't be used with a hash-like literal unless it's instantiated. A hash-like literal with an uninstantiated generic type infers the key and values types and instantiates the type with exactly those two type parameters. |
Fixes #9551
As a byproduct this also allows empty array literals like
String{}
because that's just a different syntax forString.new
.If we don't want to allow that syntax for everything, it could be further restricted. But this patch focuses on fixing a bug.