HaxeFoundation / haxe-evolution

Repository for maintaining proposal for changes to the Haxe programming language
111 stars 58 forks source link

Self access for abstracts #62

Closed markknol closed 2 years ago

markknol commented 5 years ago

Add a way to access "self" for abstracts, which is a getter for (cast this:MyAbstract).

abstract MyAbstract(MyType) as <ident> { 
}

Rendered version

nadako commented 5 years ago

I like the idea and I can totally relate to the use-cases. One thing I'm not sure about is whether we should actually generate a getter field. I would probably go with adding an identifier resolution rule instead.

markknol commented 5 years ago

I'm obviously no compiler developer so it doesn't have to be a actual field. As long as it works that way. What's a better way to describe that particular section?

nadako commented 5 years ago

I'm obviously no compiler developer so it doesn't have to be a actual field. As long as it works that way. What's a better way to describe that particular section?

I mean, that is also an option, I'm just proposing an alternative that seems a bit more clean to me (however it might be more dirty implementation-wise). If you want to also add this to the proposal, then I'd say something like:

Alternatively there could be a new identifier resolution rule:

  • When resolving and identifier while typing a non-static abstract method, consider the name, provided with as <ident>.

Although this raises more questions, as in at what place in the resolution order should it be considered, can it be overshadowed by local vars, should there be an error when declaring an argument/local var/field with the same name, etc.

RealyUniqueName commented 5 years ago

I think it should behave the same way as a field, but generate the same syntax tree as implicit cast of from Type.

Simn commented 4 years ago

This is a good idea.

~I have a question about the syntax though: Shouldn't the as be part of the underlying type? For instance, abstract Vector2d(Vector2dImpl as self). The reason is that we're referring to the underlying type, not the abstract. This would also avoid a special case for @:coreType abstracts where we have no underlying type.~ Never mind I'm retarded.

I think it should behave the same way as a field, but generate the same syntax tree as implicit cast of from Type.

I agree.

benmerckx commented 4 years ago

Isn't this referring to the underlying type, whereas self or whichever identifier would refer to it typed as the abstract?

Simn commented 4 years ago

You're right, I have no idea what I was thinking.

haxiomic commented 4 years ago

I like the concept well enough but just a nitpick: I struggle with the as <ident> syntax – It took me a while to figure out what this meant. I associate as X with X being a type, and it was unexpected that it actually referred to a new member ident because I've never seen a member declared in type's header

Maybe we don't need the ident to be variable – we could have a standard ident that's available in abstract contexts, something like thisAbstract

If onsite getters land then the original code snippet would be simpler: var self(() -> (cast this: MyAbstract), never):MyAbstract;, but I still see the value in making this a standard feature

RealyUniqueName commented 4 years ago

I associate as X with X being a type

What about in?

abstract MyAbstr(MyClasss) in <ident>
RealyUniqueName commented 4 years ago

Also let's do the same thing for the underlying type:

abstract MyAbstr(MyClass in <identUnderlying>) in <identAbstract> {

because similar trick is also used a lot if an abstract doesn't have to UnderlyingType but accepts other instances of that abstract as methods arguments:

abstract Money(Int in int) {
    @:op(A + B) @:commutative static function add(a:Money, b:Money):Money {
        return new Money(a.int + b.int);
    }

    public function new(amount:Int) {
        this = amount;
    }
}
Gama11 commented 4 years ago

I don't see how using in helps. It doesn't distuingish it from types because there's already the quasi-deprecated import <Type> in <Alias>; syntax. I think in both cases it sounds more awkward than as.

kevinresol commented 4 years ago

Can we just use the abstract keyword?

ncannasse commented 4 years ago

I'm okay with the feature but it all depends on the syntax. I would not like another magic identifier so it requires some kind of declaration. Maybe something like

@:this var self(get,never) : MyAbstract;

nanjizal commented 4 years ago

I think my use case is related... I would like to use Array Access internally within an abstract when abstracting an Array, Haxe seems fine with this[0] but not with [0], the second I am told refers to self which is not supported? The problem abstract code ..

public var red( get, set ): Float;
    function get_red(): Float {
        return [ 0 ];
        //return read( 0 );
    }
    function set_red( v: Float ): Float {
        [ 0 ] = v;
        //write( 0, v );
        return v;
    }

For example and more context, this experiment works, but I had to call methods because within the abstract it did not seem possible to use array access: http://try-haxe.mrcdk.com/#d6192 Instead it was suggested over the haxe discord channel that I use self by Gama11: http://try-haxe.mrcdk.com/#e71a1 ( rem to switch haxe ver in compiler options ). But if you notice in the generated Javascript code - there seems more overhead with a self getter/setter than in my original code, self is not optimum unless the compiler does some magic to reduce it back to my original read and write methods.

From a users perspective it would seem logical that an abstract should allow me to clarify between self and this, I really don't like some obscure @ macro stuff.

I am pretty sure if I understand Nicolas, his proposal will not be something I will forget and I will spend half an hour every time I need to use it trying to work out which subsection of the abstract documentation covers it's use or which pull request references it. I think it is far more natural to have a magic 'self'.... - it's needed for any advanced abstract use, further it should not ( ideally ) add a function call overhead as a getter setter seems to - lots of compiler magic is ideal here!!

From a language design perspective abstracts are great they allow you to structure your data more like a 'Data Orientated Design' so for example I was exploring storing my WebGL positions and colours in Arrays but then using abstracts to allow easier and cleaner access and creation.

I think 'self' as a keyword should be added since abstracts need them, self seems pretty clear term. Also I would still like to be able to iterate over abstract enums without having to google for a macro :)

anyway just my feelings on self perhaps they will provide different perspective.

Simn commented 4 years ago

We didn't reach a consensus on this one in our haxe-evolution meeting yesterday.

We agree that this feature is useful, but the syntax remains an open question.

nanjizal commented 4 years ago

I think generally abstracts suffer from a lot of 'this' compared to classes could we just use "_" to reduce noise, I think it would be really easy to remember.

_[0]
this[0]

For any cases where is not clear where it may conflict with other use the user can just define a local var however they want.

var here =_;
var abstract =_;
var self =_;

and if it has a parent abstract you can access that via

var parentAbstract = _.super;
var grandParentAbstract = _.super.super;

Well just adding ideas as really keen on an implemention, so maybe it will sporn other ideas.

nanjizal commented 4 years ago

fair enough if your not keen on that suggestion, but I don't want self problem to be ignored.

how about allowing the user to define the keyword, but they have to define it if they need self, this is not too hard to remember and it similar style to forward.

@:self('myInt') 
@:forward
abstract MyInt( Int ) from Int to Int {

Technically this is useful because you could access 'myInt' from within a child inherited, but would need to error if an inherited abstract tried to reuse the same self.

@:self('mySpecial') 
@:forward
abstract MyIntSpecial( MyInt )
nadako commented 4 years ago

Let's not overload _ with even more meanings please :)

markknol commented 4 years ago

@nanjizal You can always do abstract MyAbstract(MyType) as _ if you really want to use that 😉 That's the nice thing, you can name it however you want. However if you want super in abstract, that would require another proposal I guess. But I personally didn't have a need for that so far, because I don't have such deep layers of abstractiness.

kevinresol commented 4 years ago

Just wanna bump the idea of using the abstract keyword itself.

abstract is now/potentially used as:

I think there will be no problem if we use it in expression-level for self-access? This won't break anything because even in macro it would just be EConst(CIdent('abstract')), and it requires no addition syntax to declare (e.g. as/in/@:this as proposed by others).

What is the exact reason to reject this syntax proposal?

RealyUniqueName commented 4 years ago

We also want something for accessing this on an abstract instance. I guess if we go for abstract as proposed by Kevin, then we can use .this. I just don't like the keywords to be overloaded with even more different meanings.

nanjizal commented 4 years ago

I tried to find some other words that might be short that relate to abstract, this or similar.

virtual
unreal
notional
here
that
hereof

I think hereof is quite a fitting term, often used by Lawyers, it it easy to google, and explain. It is likely users may already use 'here' so here on it's own should be avoided.

The down side on using abstract is that it gets more complex to document with it having many meanings, and easy for new users to confuse.

Obviously there are here.. variants http://www.plainenglish.co.uk/the-a-z-of-alternative-words.html

An interesting one would be hereunder which could be used consistently to access a parent abstract.

Anyway I am just providing possibles, it's true haxe is keen to use abstract term for quite a few uses, it maybe wise not to use it up.

At least with hereto there is no alternate language programming expectation that needs to be corrected.

Aurel300 commented 3 years ago

How about:

abstract Foo(anyIdent:Int) {
  ...
}

Edit: never mind, it's the other way around, so this would be too confusing :)

haxiomic commented 3 years ago

@Aurel300 that's a pretty neat approach, seems clear enough to me

edit: or maybe not, types are backwards (see below!)

kLabz commented 3 years ago

How about:

abstract Foo(anyIdent:Int) {
  ...
}

Seems weird to me to have this be of type Int and anyIdent of type Foo with this syntax... (but inverting would be a brutal breaking change..)

grepsuzette commented 3 years ago

@Aurel300 I think this suffers the same problem as Simn proposal above. The self doesn't refer to the underlying type since it is already referenced by this.

Therefore abstract Foo(anyIdent:Int) {} feels wrong to me because it seems to associate anyIdent with Int, while the discussion is about associating it to Foo.


Personally I see no big problem with the original proposal,

abstract MyAbstract(MyType) as <ident> {}

it can be misread by a newbie (confusion with import __ as __), but a newbie will have to read the doc when he stumbles upon abstract anyway.

And:

(I didn't know the trick detailed in the rendered version, underusing the abstract feature because of that, so I'm very much in favor of some simple solution).

grepsuzette commented 3 years ago

Alternatively, since abstracts requiring a self access are far from being the majority, how about a meta like @:abstractSelf(self) abstract Foo (String) {}?

This is self-documenting and awareness of that feature can be maximized by a simple paragraph in the documentation.

kevinresol commented 3 years ago

abstracts requiring a self access are far from being the majority

Just to clarify that self access is not uncommon, I use it a lot for toString() and alike.

and wanna raise again the abstract keyword (with a snippet this time), which is not used in expression level right now and will involve no breaking change to macro API (just add CIdent('abstract'))

I just don't like the keywords to be overloaded with even more different meanings.

abstract is a noun in this context anyway (referring to the "abstract" type), so I think this self-access usage is not overloading its meaning, just reusing it.

abstract Error(Int) {
    var Forbidden = 403;
    var NotFound = 404;
    // ...

    public function toString() {
        switch abstract {
            case Forbidden: 'Forbidden';
            case NotFound: 'Not Found';
        }
    }

    public function foo(bar:Int) {
        abstract.bar(bar); // to resolve shadowed field
    }

    public function bar(v:Int) {}
}
grepsuzette commented 3 years ago

Ah suddenly I understand it with your example.

Find the abstract is an elegant solution, but it may be worth posting more examples e.g. with operator overloading because it falls into place so neatly all the question should revolve around the aesthetics with this solution: is it shocking or not.

grepsuzette commented 3 years ago

@kevinresol Ok I edit my message, my other arguments were pretty weak:

But. who's to say that one day, we won't have a use for var e = abstract { some anon } or any expression with abstract in front of an anon? If we use abstract as a special variable, we won't have it anymore as a keyword. I think this is a potential problem. I know it is far fetched (I looked too much C/C++ code maybe), but I think it is wiser to keep abstract as a reserved keyword than as a special variable name.

nanjizal commented 2 years ago

Another time I come accross this awkwardness, and there is still no decision, it's definitely a hole in Haxe typesystem that needs plugging.

https://try.haxe.org/#34fE48A9

just to clarify when I tried to switch on self compiler complained and suggested I need to cover '_' which I couldn't add or else how is the compiler helping. So I need 'self' of some form. The ( this: MyAbstract ) is really hard to remember and ugly to use for such a common situations.

I think 'thisSelf' or 'thisAbstract', although both quite long, will be easy to understand for a new user.

Likely I am being nieve but can the compiler not inject the required code when 'thisAbstract' is referenced, perhaps if a user need to allow dynamic/reflection access then some @keepThisAbstract or something, or perhaps 'thisAbstract' is always added but DCE will remove if not required.

  // implementing self to make it easier to switch.
  // ideal if compiler added this and DCE unused ones away.
  public var thisAbstract( get, never ): MyAbstract;
  public inline function get_thisAbstract(): MyAbstract {
    return ( this: MyAbstract );
  }

Seems very simple approach, it may not cover all bases but it would certainly go a long way.

nanjizal commented 2 years ago

The simple injection would be fairly quick and easy to implement? And could be improved later if it was needed.

Simn commented 2 years ago

If you read the previous comments you will find that the problem isn't the implementation but the syntax.

nanjizal commented 2 years ago

Well I was taking Kevin's post forward and trying to get some traction that some decision is needed, and 'abstract' itself is nice but problematic.

'thisAbstract' seems clearer than 'abstract', in that there will be less user confusion.

So sometimes users will want this

other times they will want thisAbstract

seems really plain simple and any users of abstract types will understand that 'this' is the internal implementation and thisAbstract is ( this, MyAbstract ). I guess one other alternative...

thisMyAbstract

but that feels a bit too magic.

There are other options I see above suggested like 'in' and more messy '@' stuff, but to me they are overly complex for average and common use case, we would spend our time googling to remember the syntax. The situation seems very simple and outlined in the very first post by markknol, we just need 'self' and just picking a good name is needed.

nanjizal commented 2 years ago

I vote anyway that next time compiler team meet there should be a decision, it's a type hole for several years now, so bumping thread a little :) But thanks anyway Simn your doing great job, even if some of us like me are annoying.

haxiomic commented 2 years ago

thisAbstract has the benefit that it can be discovered by users rather than learned through the manual; users start typing this, which if they don't know abstracts well they won't necessarily know if it refers to the abstract or the underlying type, but they will also see thisAbstract suggested, allowing them to quickly figure out which one of the two they need for their tasks

hughsando commented 2 years ago

I think the proposed solution is not too bad:

abstract Foo(asInt:Int) {

Where specifying an alternative for "this" frees up "this" to be the Foo. So kind of oldway/new way with no breaking change.

lublak commented 2 years ago

What about this.this

kLabz commented 2 years ago

I think the proposed solution is not too bad:

abstract Foo(asInt:Int) {

Where specifying an alternative for "this" frees up "this" to be the Foo. So kind of oldway/new way with no breaking change.

That's the syntax I'd want but having this referring to underlying type or the abstract itself depending on abstract declaration might be confusing. But still is my preferred alternative.

markknol commented 2 years ago

While giving this another thought. A breaking change (but probably a good/correct one) would be super to access the underlaying type and this access the self type.

Simn commented 2 years ago

While giving this another thought. A breaking change (but probably a good/correct one) would be super to access the underlaying type and this access the self type.

Frankly that's my preference at this point as well. It's pretty silly because it's clearly backwards, but maybe I can find some mental gymnastics to justify it...

Simn commented 2 years ago

Er, sorry, I read that backwards too. Of course I don't want to break it, but just use super to access the abstracts.

kLabz commented 2 years ago

Er, sorry, I read that backwards too. Of course I don't want to break it, but just use super to access the abstracts.

Doesn't make much sense though =/ I get the point about no breaking change, and it's an important one, but having this reference the abstract and super the underlying type, when used from within the abstract, is the one that makes sense, not the other way around. @hughsando's proposal at least goes in this direction; it's not perfect but I see it as more future proof.

kevinresol commented 2 years ago

While giving this another thought. A breaking change (but probably a good/correct one) would be super to access the underlaying type and this access the self type.

While this is more "semantically correct", it could cause silent breaking change:

abstract Foo({x:Int}) from {x:Int} to {x:Int} {
  var x(get, never):Int;
  function get_x() return 1;
  public function print() {
    return this.x;
  }
}
({x:0}:Foo).print(); // before change: 0; after change: 1 => breaks silently
Simn commented 2 years ago

That one seems really impractical. It would mean that you end up replacing all this whenever you want to access the abstract, which is even more wonky.

nanjizal commented 2 years ago

well with

abstract Moo( p: Int )
abstract Foo( p: Moo )

you could reference the Int with

super.super

But it's horrible to break so much existing code.

struggling to see how super could be made to work, found a rather ugly approach...

So for current this ( Moo ) there would be two options ( old ) and ( new ):

this  OR  this.super

and for Foo the abstract one.

this.abstract

then you could write from within Foo to access Int :

this.super.super

So many users could continue to use 'this' but if they wanted more control they could use 'this.super', and 'this.abstract'.

TheDrawingCoder-Gamer commented 2 years ago

i really like the abstract idea. why not reuse a keyword we already have? And it makes more sense imo. You access this abstract with abstract. About your child abstracts, that's tricky. I don't know what you should use. Maybe this.abstract ? And for the super super idk. this is just silly at this point. this.super.super could work but I'm unsure I like that syntax.

Aurel300 commented 2 years ago

Let's have a summary of all the ideas so far… For an abstract type Abs defined over T (abstract Abs(T)):

Declared as Local access to Abs Local access to T Instance access to x:Abs Instance access to x:T Breaking change Source
abstract Abs(T) as ident ident this x - N original proposal
- self this x - N original proposal, alternatives
- thisAbstract this x - N comment
abstract Abs(T) in ident ident this x - N comment
abstract Abs(T in identT) in identAbs identAbs this (or identT) x x.identT N comment
- abstract this x x.this N comment, comment
@:this var ident(get, never):MyAbstract ident this x - N comment
- _ this x - Y comment
@:self("ident") abstract Abs(T) ident this x - N comment
- virtual, unreal, notional, here, that, hereof this x - Y comment
abstract Abs(identT: T) - this (or identT) x x.identT N comment
@:abstractSelf(ident) abstract Abs(T) ident this x - N comment
- thisAbs this x - Y comment
abstract Abs(identT:T) this identT x x.identT Y comment
- this.this this x - N comment
- super this x - N comment
Simn commented 2 years ago

This proposal has been accepted, see https://haxe.org/blog/evolution-meeting-2021/ for details.