HaxeFoundation / haxe

Haxe - The Cross-Platform Toolkit
https://haxe.org
6.05k stars 647 forks source link

EitherType in function types #8171

Open andyli opened 5 years ago

andyli commented 5 years ago
class Test {
    static function test(f:haxe.extern.EitherType<Int,String>->Void) {
        f(123);
        f("abc");
    }
    static function main() {
        test(function(v:Int) {
            trace(Std.is(v, Int)); //should be always true, but actually print true then false
        });
    }
}

I think the test() call shouldn't type-check. i.e. Int->Void shouldn't unifies with haxe.extern.EitherType<Int,String>->Void.

kLabz commented 5 years ago

Unless I am missing something, I think it's expected since haxe.extern.EitherType is meant for externs likely doing their own runtime checks, so it's up to your test() function to call f appropriately.

andyli commented 5 years ago

I don't think type soundness has anything to do with designing for extern usage or not.

It is simply unsound for A->Void to unify with EitherType<A,B>->Void. It's similar to the case as follows:

var f:Float->Void = function(n:Int) {}; // can't compile,  n : Int -> Void should be Float -> Void

If I want something that can accept both A->Void and B->Void, I can write EitherType<A->Void, B->Void>.

kLabz commented 5 years ago

I would have said it's more similar to var f:Int->Void = function(i:Float) {}; (or var f:B->Void = function(a:A) {}; with B extends A), which compiles.

Either way I now see your point, but it does not seem to me to be possible to handle this specific case without breaking many things if the above examples are indeed the same thing.

andyli commented 5 years ago

more similar to var f:Int->Void = function(i:Float) {}; (or var f:B->Void = function(a:A) {}; with B extends A)

No, they are not similar, but exactly the opposite. Function types are contravariant, which means A->Void can unify with B->Void iff B unifies with A, but not the opposite.

var f:Float->Void = function(n:Int) {}; //can't compile, and it's correct for the compiler not to compile
var f:Int->Void = function(f:Float) {}; //compiles, and it's correct too
Simn commented 5 years ago

I agree that this violates variance because the EitherType is the more general type.

kLabz commented 5 years ago

Seems like I understood it backwards, sorry about that :confused:

Simn commented 5 years ago

If I apologized every time I got tripped by function argument variance you'd think I was Canadian.

Simn commented 5 years ago

The underlying problem here is that EitherType has to T1 to T2 which makes it unify in this direction. I'm not sure if we could just change that...

RealyUniqueName commented 5 years ago

Shouldn't we check that the argument of the closure is compatible with every to of the abstract?

Simn commented 5 years ago

Is that even possible? If that was the case then we wouldn't need the abstract in the first place...

RealyUniqueName commented 5 years ago

Probably possible in some rare situations with similar abstract. E.g. Either3<T1,T2,T3>