-
-
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
Explosive birth of automatic casts! #6074
Conversation
This is great! Now C bindings code more natural to write. |
x | ||
end | ||
|
||
foo(2147483647_i64) |
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.
Why does this work?
I would expect that if the literal is explicitely typed, that type should be honored
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.
Yeah, maybe that shouldn't work, but I don't know, there's no harm in casting the value.
Plus, right now there's no concept of a number that's explicitly typed. Or, well, there's no difference between 1
and 1_i32
, both parse to exactly the same thing, and are handled in the same way in the compiler. So maybe we should start making that distinction and only apply the rule for values that don't have a suffix...
It should be easy to implement, I might try that tomorrow. But I don't think this particular thing should block the entire feature from being merged.
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.
Seems okay as long as type on a method or C fun is honored.
Welcome back @asterite 🎉 🕺 |
I have to admit, I danced around the room a lot when I saw this pr. My only concern is syntax: reusing symbol syntax for enum shorthand vs keeping type syntax and just omitting the full path to the enum type. I'm conflicted. I think a newcomer would see symbols being "converted" to an enum and be confused. |
@RX14 I too was weirded out by the symbols, but paint(Red) makes you think about name spacing issue |
@RX14 Yeah, that's the only concern about symbol -> enum: confusion. Maybe we can have a separate syntax... or we could simply remove symbols from the language and just use them for this. But given that there's no ambiguity with this syntax, maybe it's fine. Well, there's the "ambiguity" of having a method receiving both a Symbol and an Enum, but in that case the Symbol overload wins, of course. But I don't think anyone will write such code... The other proposal about using And then, in Ruby it's very common to pass a symbol as an option. |
My preference would be for |
Yeah, Plus I wouldn't know how to implement it. To solve a call, all of its arguments must be typed. But if Another option is to do something similar to what Swift does: prefix it with a dot. So: "foo".upcase(.Ascii) The type of But I don't know, I still prefer the symbol syntax. |
Cool, this PR also makes the following work (maybe still a bit controversial for enums, but type safe 😛): a = [] of Float64
a << 1
a << 2
p a # => [1.0, 2.0]
enum Color
Red
Green
Blue
end
b = [] of Color
b << :red
b << :green
b << :blue
p b # => [Red, Green, Blue]
h = {} of Color => Float64
h[:red] = 1
p h # => {Red => 1.0} Hey, it even makes this work: # All of these currently give an error
[1, 2, 3] of Float64
[1, 2, 3] of UInt8
{1 => 2} of Float64
Set(Float64){1, 2, 3} That's because the code is expanded to a series of calls. And of course... enum Color
Red
Green
Blue
end
[:red, :green, :blue] of Color This feature is definitely more powerful than the snippets I wrote in the beginning of this PR. |
@asterite I assume swift chose it's syntax because for swift So yeah, if using I love all of those snippets @asterite! |
IMO there's way less ambiguity with |
I like the idea to use a uppercase symbol and only match the exact same name as the enum member (case sensitive, camel case): |
I like the symbol syntax with the same casing as the enum. Makes it super clear this is no "ordinary" symbol :) But I could also be ok with ordinary snake_case symbols as well. Either one is a huge improvement IMO. A few ideas on making it easy for people to figure out what went wrong when using a symbol:
I think this would help eliminate some of the confusion that will surely come up and will stop people from banging their heads too long on typos |
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.
This is perfect to me. Casting literals is great, but casting symbols as enum values too is 😍
Maybe just the autocast of an explicitly typed literal is a little surprising, but that can be changed later (or not).
Let's merge, merge, merge! 🎉 |
end | ||
end | ||
|
||
raise "Bug: expected to find enum member of #{to_type} matching symbol #{symbol}" |
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.
Some ideas for this error message: #6074 (comment)
Could easily be done in a future PR if you'd prefer to get this in ASAP
What happens in the case of enum Foo
Foo
FOO
end what does This, and readability, makes me want to make the symbols case sensitive. |
|
@RX14 Note that we already define query methods for enums, so there's already a conflict in that snippet ( After that, there's no conflict about using the underscore version or the equal fersion. We still have: module Color
Red
Green
Blue
CoolOne
end
color = Color::Red
color.red? # => true
color.cool_one? # => false So if we have We could go with
This is not trivial to implement. Maybe even harder than doing this whole PR because it involves heuristics. There might be multiple overloads that didn't match, and then we' have to check if any of the restrictions solve to an enum (maybe the restriction also involves other types in a union), and so on. I'd like to leave this for a separate PR. |
That makes a lot of sense 👍 I'm so excited to see this and I agree underscores do look a lot better, but maybe that's because I've done so much Ruby/Elixir :) |
That's why I'd like to have some slightly different visuals, to express that the argument is not just a regular symbol but in fact the short notation of an enum member. |
OK i'm convinced about the problem already existing for the query methods, so there's no technical reason for |
The way I see it, symbols have very few use cases, enums are almost always better. And mixins APIs with symbols and enums is not common. In fact, I believe not a single public API method in the standard library uses symbols, at all. So I don't see a conflict of using symbols for this. Again, we could go as far as removing symbols from the language, though that might not be necessary. |
I'm starting to like the idea of matching the symbol with the enum name. It means there's only one way to match the enum member, and also it's easier to search the enum from the member's name. It should also be just slightly faster to match in the compiler. Plus it does look like a shortcut form of the enum member. But then, I'd like to get rid of the enum question methods, because they use the "undercase" variant. It would be nice to always have to use the member name. Using This works though: struct Enum
def is?(other : self)
self == other
end
end
enum Color
Red
Green
Blue
end
color : Color
color = :Red
if color.is?(:Red)
puts "Red"
end
case color
when .is?(:Red)
puts "Red"
when .is?(:Blue)
puts "Blue"
when .is?(:Green)
puts "Green"
end But maybe that's too much to type? Maybe it's fine. Or maybe people will write We could hardcode a rule for Or we could just leave the current question methods... |
I personally think that isn't much more to type and is far less "magical". It's incredibly clear 👍 |
@asterite perhaps it'd be better just to not mention imperceptible performance changes, and only mention performance where you think it's an important topic, because it can be assumed that every feature will have some performance impact. |
Would be sweet if autocasting would also work in cases below: require "logger"
def foo?(level : Logger::Severity)
# ...
end
# works
foo? :info
# doesn't work & shouldn't
foo? :nonexistant
# doesn't work & should
foo? true ? :error : :info
foo? x = :error Instead, it fails with:
If not possible, it would be helpful to tweak compile error message at least, to something more informative and less misleading than the current one. |
I said 💩 . Don't mind me. |
@Sija from the OP:
The limitation was expressed in the OP, there is lot of complexity in trying to achieve that (also mentioned the current complexity of this). Cheers. |
The problem with the error messages is that we're using the same syntax for two ideas: symbols and enum values. I'd still like to discuss ways of removing symbols, to make the I think really the main blocker for removing symbols is the design of named tuples... |
Let's remove symbols, and make named tuples work only with strings. |
Hey @asterite, why don't you create a discussion for removing symbols and leave them for Enums only? I'm sure you're able to bullet some strong points. |
@vladfaust the point of removing the |
@j8r point is to have a shorthand for specifiying enum members without a need to pass whole path ( |
What
This PR lets the compiler match number literals against type restrictions that don't exactly match the expected type, but since the literal's value fits the type, it is automatically cast.
Additionally, a similar behaviour is provided to specify enum values by using a symbol.
All of this works as long as the call doesn't become ambiguous. The implementation was a bit tricky, but finally, and amazingly, it worked.
For a quick understanding of the first part (numbers), you can read what's being requested in #2995. With this PR, all of those snippets now compile.
But I'll write a few snippets here.
Method type restrictions
Numbers
Symbols -> Enums
Instance variables
Class variables
Local variables
Method default values, with restriction
Combined
Some examples with the current standard library
File
Process
String
What if it's ambiguous?
When it doesn't work
All of the above works only when passed directly to a method, or assigned directly to an instance/class variable. When putting intermediary expressions, or intermediary assignments, it stops working. But I think that's OK. We simply must document these rules. I also believe for example in Go you can pass number literals like that, but of course it stops working when you introduce intermediate expressions too.
Why
I can easily say this might be one of the most wanted features in Crystal. Having to type those number suffixes for no real reason, when the compiler could figure this out, or having to type long enum values, is super tedious, and kind of goes against Crystal's philosophy.
Converting symbols to enums might be controversial.
The important thing to understand here is that this is all sound and type safe. If I pass a symbol to a method expecting an enum, and the name can be matched (currently
underscore
is being invoked on both ends), then there's simply no confusion: the symbol must mean that enum member. If there's a typo in the symbol you get an error, exactly like what happens with the full enum member. The big advantage is that you get to type less, and also read less redundant information.With the rule for numbers you can more easily write numeric and scientific code, and also code for games.
With the rule for symbols -> enum, one is actually more encouraged to use enums, because using them in APIs, when having to choose one option between many, one can still pass a symbol (but you'll have to type the full enum value if choosing the value dynamically, but that's as bad as now).
How
Implementing this was tricky.
First I started with number literals. My first idea was to give them a different type that would be compatible with other number types if the literal's value fits. That worked, but then this happened:
So I thought I could detect the ambiguous case, and that didn't work at all, because
foo(1)
still matches all overloads.The solution then came: first try to match methods the old, usual way. If that matches, then all is good and should work like before. If no match is found, only then try again with this special types for literals. If just one matches (or, well, multiple overloads can match, but all of them must match against the same integer type... think about union types in other positions and how that can create a multiple dispatch), then we are good to go. If more than one matches (against different integer types), then it's an ambiguous call.
This is not the most efficient way to do it because when the rule applies, we end up doing two method lookups instead of one. But I think the number of places in a program where this will be used will be much less than the total number of calls where this rule won't be used. And, in any case, this just adds a small performance cost but compilation speed isn't important at this point.
After that, adding the symbol -> enum rule was simple.
Then I had to make sure the rule worked in all of the cases I documented above, which was a bit tedious, but it finally worked.
Fixes #2995
Fixes #6067