HaxeFoundation / haxe.org-comments

Repository to collect comments of our haxe.org websites
2 stars 2 forks source link

[haxe.org/manual] Type Parameters (Type System) #139

Open utterances-bot opened 9 months ago

utterances-bot commented 9 months ago

Type Parameters (Type System) - Haxe - The Cross-platform Toolkit

Haxe is an open source toolkit based on a modern, high level, strictly typed programming language.

https://haxe.org/manual/type-system-type-parameters.html

theJenix commented 9 months ago

What is the right way to declare and reference static members inside classes with type parameters? For example, I have a class and I'm implementing a Null object for that class:

class MyClass<T> {
    public static final Null = new MyClass<T>();
}

If I instantiate a MyClass<String> I cannot compare it with MyClass.Null:


var myStr = new MyClass<String>();
if (myStr == MyClass.Null) { // this line gives an error

}
ibilon commented 9 months ago

@theJenix The type parameter only exists for the instance fields, you can see static fields as global variables created before the main function is run, they can't use the type parameter because they are "outside the class".

Simn commented 9 months ago

Uhm, that shouldn't even compile...

theJenix commented 9 months ago

Thanks @ibilon and @Simn! For what it's worth, this is an intersection of two very common language patterns, e.g. parametrized types and the Null Object pattern. It would be nice if the docs had some discussion about what the "right" way to achieve this is, as it's very possible in other similar languages.

For example, I made this work for me by declaring Null as an instance variable (not static); that feels a little heavy because now every MyClass will have the overhead of it's own Null object. Is that the recommended "right way" do implement this in Haxe? Or is there another, cleaner way to do this?

Simn commented 9 months ago

The concept of a null-value for a parameterized type seems dubious to me. You can always just initialize it to new MyClass() without specifying the type parameter, but then you'll end up with MyClass<Unknown> and that will unify with the first type it is assigned to/from. If you want it to unify with everything then you'll likely want MyClass<Dynamic>.

Could you give an example of another language that supports this? I'd like to understand the semantics you're after because this could involve anything from bottom types to existential type parameters.

theJenix commented 9 months ago

Sure. The first one that comes to mind is C#:

    class MyClass<T> {
        public static MyClass<T> Null = new MyClass<T>();

        public MyClass<T> get() {
            return MyClass<T>.Null;
        }
    }

    class Test {
        bool testMe() {
            return new MyClass<string>().get() == MyClass<string>.Null;
        }
    }

In this case, T is accessible by static items as part of the class; in fact, in get() I can just return Null instead of qualifying it with the class name and it still compiles. I think this means that Null is static across all MyClass (for a specific value of T), and not globally static, but this is still probably preferable to a Null per instance.

Typescript also supports a version of this:


class MyClass<T> {
    static Null = new MyClass()

    get():MyClass<T> {
        return MyClass.Null;
    }
}

function testMe() {
    return new MyClass<number>().get() === MyClass.Null && new MyClass<string>().get() === MyClass.Null
}

In this case, new MyClass() actually does end up at MyClass but that seems to suffice for being able to return the null object from get() and it satisfies a strict === test.

As some added context, the reason I am exploring this pattern in the first place is while normally I appreciate the struct null safety in Haxe, sometimes it can be a little too aggressive so in some cases. Rather than deal with it, sometimes it makes sense to just not use null and instead use a null object but maybe improving the null safety checker would be a better overall experience here?

Simn commented 9 months ago

In the case of C# this makes some sense because it has real generics, so something similar to Haxe's @:generic.

The Typescript version looks like it would just be MyClass<Dynamic> in Haxe, though I wonder how their typing works for cases like this. Basically, what is the type of MyClass.Null itself, without any other operations? I suspect that they treat its type parameter like an any-type.

theJenix commented 9 months ago

This is what the Typescript plugin in VS Code reports: image.

Which I think tracks with what you're saying re MyClass. I could change the typescript code to new MyClass<never>() or new MyClass<any>() or even new MyClass<string>() and it still functions as described above; I'm guessing the type param is ignored when used in this way.

Incidentally, I was checking thru some older code of mine and it looks like I had previously implemented this pattern by declaring Null to be of type Dynamic (not MyClass<Dynamic>). If I try using MyClass<Dynamic> for the type of Null in this case, the compiler complains when I then try and pass Null in for a concrete type, e.g. MyClass<MyOtherClass>. I'm guessing because "Dynamic" is not factored in when checking for invariance.

Ultimately, the goal here is to have a single object that can represent null and possibly stand in for the the "real object" to write code that is free of null reference exceptions, so declaring a Null variable of type Dynamic and then ensuring that it is populated with the correct type of object isn't bad. If I could declare it as MyClass<Dynamic>for that added bit of type safety, that would be better.

Also, I tried experimenting with @:generic but it looks like we can't have static fields inside a generic type; making that work could maybe also be a workable solution here, letting me decide if I really want a type safe static object inside a parameterized type.

In either case, I think it would be helpful to add a note on this page talking about this and/or similar use cases, or at least acknowledging that this concept is not supported. There's a Trivia note that talks about why we cannot use angle brackets in expressions; maybe something on the level of that?

c-g-dev commented 9 months ago

Regarding the trivia section at the end "...why a method with type parameters cannot be called as method<String>(x) ...", is there no other way to supply the method generic parameter like this, other than passing in an object and letting it be implicitly inferenced?

Something like:

    var data: Dynamic;
    function getDataLike<T>(): T {
        return cast data;
    }

    var view = getDataLike<{myInfoView: String}>();
kLabz commented 9 months ago

var view:{myInfoView: String} = getDataLike(); would work for this example