Right now one can write:
def foo(x : Int32 | String)
end
def foo(x : String?)
end
def foo(x : Void*)
end
def foo(x : Int32 -> String)
end
def foo(x : {Int32, Int32})
end
However, one cannot do the same in regular code:
Int32 | String # this one actually works because we define the `|` method on class
String? # syntax error
Void* # syntax error
Int32 -> String # syntax error
{Int32, Int32} # works, but has a different meaning, it's a tuple of types, not the class of a tuple type
One can do, in some cases:
Pointer(Void)
Proc(Int32, String)
Tuple(Int32, Int32)
but no such thing exists for union, because union is a special type (at runtime a variable can't be of a union type, just of a single type).
I propose to introduce a syntax to refer to these types in regular code with this short syntax:
::(Int32 | String)
::(String?)
::(Void*)
::(Int32 -> String)
::({Int32, Int32})
# for this last case we can probably also allow:
::{Int32, Int32}
This isn't a super needed feature, but I'd like to include it for completeness.
I would like to seem some usecases.
Alternatively, what if we make Int32 | String
always parse as a union, never as a method call? One could still do Int32.|(String)
if needed. However, we already define Class#|
in the standard library so it's unlikely someone will override its meaning.
We can also add Int32?
. One very good use case is #2714.
I like the proposal, but I'm not sure about the syntax itself. Maybe just start with adding long names, such as Union(Int32 | String)
or Maybe(String)
, until we agree on a shorthand syntax?
@spalladino I thought about it. All other types have a name, so union should probably have one two, and it's variadic like a tuple. The problem is, a union is a bit special. To support Union(Int32 | String)
there would need to be a type Union
in the language, maybe something like this:
struct Union(*T)
end
The difference with other types, though, is that it doesn't have methods: the compiler does something special when the receiver is a union, and you can't do x.is_a?(Union)
because it's never a union at runtime.
I still think we could maybe make it work with a few special rules in the compiler. Alternatively, we can make Int32 | String
always be a union and never a call, and we don't have to have a (different :-P) special case in the compiler/language.
Alternatively, we can make Int32 | String always be a union and never a call
Hmm, I don't quite get this. Will |
not be an operator anymore? Or only for constish lexed? So foo | SOME_FLAG
wouldn't work? I'm quite tired atm, so bare with me if I missed something obvious..
Only the first case is parsed as a union:
Type | OtherType # parsed as union
foo | bar # parsed as method call
Type | bar # syntax error
bar | Type # parsed as call
1 | Type # parsed as call
# etc.
Of course these too:
Foo(T) | Bar(X)
Foo::Bar | Foo::Baz
That is, invoking Class#|
won't be possible like that. One can still do:
x = Foo
x | Bar # x.|(Foo)
with the same meaning.
What about
FOO = 1
BAR = 2
FOO | BAR
?
馃憤 for parsing Union(Foo, Bar)
as the type Foo | Bar
until we agree on a short syntax and use it in the JSON mapping.
@spalladino , @asterite i like Union(Foo, Bar)
, but not Union(Foo | Bar)
.
Ouch, good point.
We could make what @spalladino suggests by implementing it in the parser itself. Basically Union(X | Y)
would parse to X | Y
as a union. That way one can't declare a Union
class, but I'd be fine with that. Thoughts?
(seems @bcardiff read my thoughts)
I think Union(X | Y)
is more familiar than Union(X, Y)
, mostly because X | Y
already means a union in some places.
Or we can support both forms... hmmm... Maybe Union(X, Y)
is more correct after all, because it's also similar to Tuple(X, Y)
, so it feels like a generic instantiation.
@asterite and Union(... | ...)
that does not play nice with splats ;-)
How about also parsing String?
to mean Union(String, Nil)
. The only conflict that can happen if one uses a class as a condition:
String? foo : bar
but I guess that's not common... and it's also useless (always truthy).
Then we would also need to parse Foo*
as Pointer(Foo)
for consistency and would still have two special cases and more verbose syntax for other cases that have special syntax in the type language. I think it's more hassle than it's worth, let's keep special syntax to the type language.
I think nilable types are pretty common and used in regular crystal code, while pointers are not. One can of course do Union(String, Nil)
for a nilable type. We can start with that. (you do like the special Union
syntax?)
For now I'm +1 for Union(String, Nil)
and -1 for Union(String | Nil)
or Union(String?)
.
How about Union(String, Nil)
being the same as Maybe(String)
?
I don't think we should consume Maybe
for this just yet :)
Just a last note about this. One now can do Union(Int32)
, which is equivalent to Int32
. That means that one can also do:
Union(Void*)
Union(Int32 -> String)
# etc.
so this is actually a way to enter the type grammar. It's maybe a bit hacky (_maybe_), but it's valid. The compiler does this all the time, when it needs to merge the type of two expression is says "give me the union of these", and if they happen to be the same it just returns the single type. Union(T)
would be somewhat similar.
We can later think of a nicer syntax for this, but this isn't something really needed, you can always refer to a type by its name.
Most helpful comment
馃憤 for parsing
Union(Foo, Bar)
as the typeFoo | Bar
until we agree on a short syntax and use it in the JSON mapping.@spalladino , @asterite i like
Union(Foo, Bar)
, but notUnion(Foo | Bar)
.