Structural typing - type compatibility

Why is A here not compatible with B?

typedef A = {
    a:String,
}

typedef B = {
    > A,
    ?b:String,
}

class Test {

    public static function main() {
        var a:A = {a: "a"};
        var b:B = a;
    }

}

Error: A should be B; { a : String } has no field b

Imagine if this was allowed (i.e. if the cast expressions were not required):

typedef A = {
	a:String,
}

typedef B = {
	> A,
	?b:String,
}

typedef C = {
	> A,
	?b:Int,
}

class Main {
	public static function main() {
		var a:A = {a: "a"};
		var b:B = cast a;
		var c:C = cast a;
		b.b = "foo";
		$type(c.b); // Null<Int>
		trace(c.b); // ... but its value is "foo"
	}
}

This would be a type-hole because you end up with a field that is supposed to be an Int but has a String value, which isn’t exactly valid on real targets.

3 Likes

I tried to get around this limitation by wrapping the type in abstract and using implicit casts, but that breaks the static extension.

I don’t know, I feel like I’m losing a lot in flexibility with those limitations and getting very little or nothing in safety in return. On the other hand, I understand that many people are willing to sacrifice flexibility to have the type system as sound as possible.
Maybe there should be some kind of option to allow the user to (ab)use the flexibility if they know what they’re doing?

I think that’s called cast? :wink:

Implicit cast through abstracts - yes, in theory - it works until you try to use the static extension (like I said in a previous comment). Explicit cast everywhere - well, no :slight_smile:

Between Dynamic, cast, macros and untyped, Haxe already offers more than enough tools for people who really want to shoot themselves in the foot. Outside of that, no serious compiler should admit typed code where a string value is stored in a place that is supposed to be an integer.

I don’t agree with that. Your example is not realistic in any sane structurally typed codebase, so the protection there is only theoretical. I think it would be nice if there’s an optional way to do this things, which doesn’t suck (like using Dynamic/untyped or explicit casts everywhere).

I understand why it’s designed this way, but over the years I’ve read comments from people ditching Haxe after becoming frustrated with inconsistencies of things looking nice in theory, but falling apart when you actually try to use them together. They kind of do have a point - just examples from this thread: You can use structural typing, you can use optional fields, but not together (unless you define the structure inline, then is fine). You can use abstracts, which do support implicit casting, you can use static extension, but again, if you try to use them together, things falls apart.

I’m sorry if this sounds confrontational, that is not my intention. I like Haxe and I think it has great potential, but this community habit of dismissal/hostility to any, even smallest criticism is not doing it any good.

You asked why the example you gave caused a compilation error and I explained the reasoning. If you consider that hostility then I really don’t know what to tell you.

Ok, I’m also curious about the reasoning behind the incompatibility of static extension and implicit casts and if there’s a way to make them work together (it may be also useful to someone who stumbles upon this thread in the future)?

Some sample code of what exactly you mean would help.

Ok, I’ll use the same example:

using Test;

typedef A = {
    a:String,
}

typedef BT = {
    > A,
    ?b:String,
}

@:forward
abstract B(BT) from BT to BT {
    @:from
    static public function fromA(a:A) {
        return cast a;
    }
  
    @:to
    public function toA():A {
      return cast this;
    }
}

class Test {

    public static function main() {
        var a:A = {a: "a"};
        var b:B = a; //now this works
        b.fn(); //this also works
        fn(a); //this also works fine
        a.fn(); //this fails: A has no field fn
    }

    public static function fn(b:B) {}

}

Hmm, I imagine supporting @:using on @:from / @:to-casts might cover that use case, without (automatically) causing the problems I explained in the github issue.


Perhaps, if you could explain your actual use case, you could get answers that get you ahead without requiring changes to the type system :wink:

Ok, that could work.

The actual use case is pretty much the same - just different type names and fields. I know that I could redesign the code in some other way, that’s not the point. I’m trying to see if Haxe could be used that way.
I’ve been coding for a very long time in both static in dynamic languages and I see a good,flexible structural type system as a nice balance between those two. And I’m not alone in this line of thinking, these days a lot of people have tasted more flexible typing systems and will look for something similar in their next language.

We’re not going to make changes to the type system off some nebulous notion of some people wanting something. If you have a concrete suggestion and can outline the design and advantages, head on over to GitHub - HaxeFoundation/haxe-evolution: Repository for maintaining proposal for changes to the Haxe programming language and draft a proposal.

1 Like

Yeah, people have been trying to do that with various things and you guys have been equally dismissive&hostile (I won’t post any links, but anyone can easily find examples).
Anyway, just to be clear - I didn’t ask for a change in a type system. I was just asking if what I was doing is possible in this language and if there are workarounds. The part you quoted was reply to @back2dos’s question.

Great way to welcome someone into the community.

I’m quite confused about what’s going on here, but it appears that you interpret disagreement as hostility…

You asked why this doesn’t work and I explained the reason. Then you asked for an option to change the behavior and I disagreed with providing such an option, but then directed you to the place to make such a proposal. And now you’re saying that you didn’t want a change after all and somehow don’t feel welcome in this community.

Huh…

1 Like

There’s a difference between changing the whole type system and implementing some simple additional option like @back2dos suggested. At least I hope that’ the case.

What I suggested will not solve your problem, only make your workaround slightly easier to work with. If you’d come forward with an actual use case, it may be possible to point you to a less awkward one. Poopooing people as unwelcoming is unlikely to get you any closer to a solution :stuck_out_tongue:

What would be the procedure to request this feature? Is it enough to mention it here or this also needs a proposal?

I would hope that the change I suggested is small enough to be handled via Issues · HaxeFoundation/haxe · GitHub

After all, it’s about selectively restoring previously available functionality.