Skip to content
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

[RFC] Support autocasting of tuple literals instead of Tuple covariance #10036

Open
HertzDevil opened this issue Dec 5, 2020 · 3 comments
Open

Comments

@HertzDevil
Copy link
Contributor

The same arguments apply to both Tuple and NamedTuple, but here only examples for the former are shown. The following code seems like it should work:

def foo(x : T, y : U) forall T, U
  (true ? x : y).as(T)
end

But if T and U are Tuples of the same size, then due to #2576 the code will fail to compile, and if the call is replaced by as?, the conversion will fail at runtime. The following will also not work:

def bar(x : {Int32} | {String})
end

bar(true ? {1} : {"a"})
# Error: no overload matches 'bar' with type Tuple(Int32 | String)
#
# Overloads are:
#  - bar(x : ::Tuple(Int32) | ::Tuple(String))

The difference here is that due to #6342 the type restriction never coalesces to {Int32 | String}.

I propose we should remove Tuple's variance behaviour and apply automatic casts for tuple literals instead, in the same way number literals autocast to other numbers, or symbol literals autocast to enum values. Consider the original example from #2576 again:

ary = [] of {Int32 | String, Char}
ary << {1, 'a'}

Here the deduced type of the {1, 'a'} literal is Tuple(Int32, Char), while Array#<< expects a Tuple(Int32 | String, Char). If the tuple literal has the same size as the tuple in the type restriction, and every element expression is compatible with the corresponding expected type, then an automatic cast is performed, so that the above will still work even without Tuple's covariance. On the other hand, we will now reject code like below, as compatibility does not imply covariance:

x = 1
ary = [] of {Int32 | String, Char}
ary << {x, 'a'} # no overload matches 'Array(Tuple(Int32 | String, Char))#<<' with type Tuple(Int32, Char)
ary << {x.as(Int32 | String), Char} # okay

Allowing compatible types instead of subtypes has further consequences, even when variance isn't involved, because compatibility checks will then be applied recursively: (both examples already work when the 1-tuples are removed)

ary = [] of {Proc(Nil)}
ary << { ->() { 1 } }
typeof(ary.first) # => Tuple(Proc(Nil))
# without tuple autocasting: Tuple(Proc(Int32) | Proc(Nil))

ary = [] of {Int32}
ary << {0_u8}
typeof(ary.first) # => Tuple(Int32)
# without tuple autocasting: no overload matches 'Array(Tuple(Int32))#<<' with type Tuple(UInt8)
@HertzDevil
Copy link
Contributor Author

HertzDevil commented Dec 6, 2020

Also continuing from #2576:

class Foo; end
class Bar < Foo; end

ary = [] of {Foo}
ary << {Bar.new}         # should not work
ary << {Bar.new.as(Foo)} # okay

Because Bar < Foo does not imply Tuple(Bar) < Tuple(Foo) anymore if Tuple's covariance is removed. In other words, the example in #7225 (comment) is supposed to break, and that PR should be reintroduced, in which case we could also reject the similar illegal code:

class Foo; end
class Bar < Foo; end

ary = [] of Array(Foo)
ary << [Bar.new]         # should not work
ary << [Bar.new.as(Foo)] # okay

@asterite
Copy link
Member

asterite commented Dec 6, 2020

I don't think this is okay, a Bar is a Foo and so it should be allowed. If UInt8 is allowed to be passed into Int32 inside a tuple, that's a bug.

@HertzDevil
Copy link
Contributor Author

HertzDevil commented Dec 6, 2020

a Bar is a Foo and so it should be allowed

It might be possible to extend the rules here so that autocasting a tuple literal consisders both covariant and compatible expressions. I am unsure if anything bad could happen that way.

Also array literals could technically autocast in the same manner, but they have an of part so things might work differently compared to tuple literals.

If UInt8 is allowed to be passed into Int32 inside a tuple, that's a bug.

It isn't any more special than passing a UInt8 literal into Int32 directly, the only difference here is that autocasting applies to member literals recursively. Things that aren't literals, like variables and calls, will continue to be rejected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants