-
-
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 T?
as Union(T, Nil)
#2742
Conversation
Not everything is about usefulness, language consistency and coherence is as important, it allows you to predict what you can expect from a language, it allows you to abstract away different features into general mechanisms of the language. The more special cases there are, the more exceptions to a general rule, the harder it gets to keep those abstract concepts together and thus the harder it gets to fully comprehend a language. Sometimes that means rejecting a convenience since it would depend on too many more additions needed to make the big picture coherent again, to introduce the "abstract concept". |
I agree with what you say, every new rule we add to the language could introduce additional complexity and make things not coherent. The way I see it, we then have three options:
So, However, the Then, some other languages also use And, if Crystal is their first language, or they are not familiar with nilable types, they will still see Now, the use cases:
A bit more of point 1. Check these codes, which one looks more consistent: # 1
class Foo
JSON.mapping({
x: Int32,
y: String | Nil,
})
property other : String?
end
# 2
class Foo
JSON.mapping({
x: Int32,
y: String?,
})
property other : String?
end If we don't introduce Finally, making Anyway, I'll leave this PR floating here a bit more until we gather more opinion and evidence on this. |
To clarify, the type grammar already has more special rules:
The inconsistency would be to port one of these to the regular grammar, but the others. In fact porting the last two is probably impossible, they are already valid but mean different things. You simply move the "Oh, it's not the type grammar" from |
In common syntax, I agree |
I understand. However, I think nilable types are more useful and used in general, as opposed to pointers, tuples and named tuples. I can also imagine wanting to read a nilable int from a database, or from a JSON, but not a nilable tuple, as these types usually don't exist in databases, JSON, etc. There's on more, proc types, like |
@@ -1142,6 +1142,10 @@ describe "Parser" do | |||
|
|||
it_parses "Foo.foo(count: 3).bar { }", Call.new(Call.new("Foo".path, "foo", named_args: [NamedArgument.new("count", 3.int32)]), "bar", block: Block.new) | |||
|
|||
it_parses "Foo?", Crystal::Generic.new(Path.global("Union"), ["Foo".path, Path.global("Nil")] of ASTNode) | |||
it_parses "Foo::Bar?", Crystal::Generic.new(Path.global("Union"), [Path.new(%w(Foo Bar)), Path.global("Nil")] of ASTNode) | |||
it_parses "Foo(T)?", Crystal::Generic.new(Path.global("Union"), [Generic.new("Foo".path, ["T".path] of ASTNode), Path.global("Nil")] of ASTNode) |
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.
Foo??
and macro expanded types {{T}}?
might be other nice test cases to be added.
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 added the Foo??
(and Foo????
) cases, but {{T}}?
shouldn't matter, that won't parse as {{T}}
is not a type, and if inside a macro then it'll work because it will get expanded to something that later will be parsed.
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.
Unless T
is a union itself since it is expanded as R|S
and not Union(R, S)
. Maybe union to_s should be always be expanded to Union(...)
syntax, unless a flag in the #to_s
is given, like the one that skips parens...
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.
Maybe. That doesn't save you when someone sends 'A|B' to a macro, because that's a call. The macro definition can use an explicit Union for this. This is what JSON mapping does.
9746094
to
4fc32da
Compare
4fc32da
to
514c843
Compare
Merged in 40c6f73 (I know some were against this, but it has many use cases and it'll eventually be more clear) |
The reason we'd want to introduce this is that now we can do
Union(String, Nil)
to refer to that union type in regular syntax, but nilable types are more commonly written asString?
in other places where a type is expected. Now thatUnion
can participate in many more places as it's a first class citizen, and given that nilable types are very common, maybe this change in syntax is good.Note that
T?
andT ?
are parsed differently: the first is a nilable type, the second is the ternary operator withT
as a condition (of courseif_true : if_false
must follow, otherwise it's a syntax error). So this still allows using the previous "feature": using a type as a conditional (though that isn't useful by itself).One could suggest adding
T*
after this, but this wouldn't be as useful asT?
because pointers are unsafe, low-level.