Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Add hook and extension variables features #571

Closed
Trokkin opened this issue Nov 11, 2017 · 14 comments
Closed

Add hook and extension variables features #571

Trokkin opened this issue Nov 11, 2017 · 14 comments

Comments

@Trokkin
Copy link

Trokkin commented Nov 11, 2017

I'd wanted to see any of these new possibilities in Wurst. I believe they're very helpful in some rare occasions to keep code clean and well-organized, at least for me.
For extension variables I mean something like UnitWrapper ... int UnitWrapper.killCount ... u.killCount += 1 as an example. I think that their initialization can be hooked to construct and onDestroy (like hook UnitWrapper.construct /n/t killCount = 0 for my example). And I mean they're restricted to class types only.
Probably they could be allowed only with special flags like hookable and extensible.

@Cokemonkey11
Copy link
Collaborator

I'm not sure I understand what extension variables are. Can you point out what this is in other programming languages?

@Trokkin
Copy link
Author

Trokkin commented Nov 13, 2017

I don't know that for other languages, that's just a concept appeared in my head while I was working with extension functions, with notice that it should be really easy to implement into jass, due to specific JASS' array structure. For other languages where classes are allocated as a solid block in memory rather than just index in set of big array, think it would be hard both to implement and to use.
I think of it as a syntax sugar for an array X and extension functions setX and getX, without inlineable function overhead and ()-less syntax.

@Cokemonkey11
Copy link
Collaborator

It sounds like it has the potential to be the basis for an interesting language feature, but if we would offer it, I would be keen to drive for doing it right. Someone with strong category theory/functional programming knowledge might have an opinion about how to provide something abstract enough to be useful in a more general sense, while still solving your use case.

@lep may I ask for your opinion?

@peq
Copy link
Collaborator

peq commented Nov 14, 2017 via email

@Cokemonkey11
Copy link
Collaborator

FWIW I don't think I would realize the value of this feature, and I'm hesitant to like language features that don't exist anywhere else.

My goal with asking lep was hopes that there might be something similar in the form of frameworks/libraries (e.g. scala shapeless is very expansive), which might be a healthier language feature than something so specific and concrete.

@peq
Copy link
Collaborator

peq commented Nov 14, 2017

FWIW I don't think I would realize the value of this feature, and I'm hesitant to like language features that don't exist anywhere else.

I agree, but it is difficult to have a workaround with the current Wurst features.

In many other languages you just use a Hashmap to attach something to an object, but this does not work in Wurst since object IDs are reused. Java has the special WeakReference to handle this together with GC. We could also add something like WeakReference (automatically setting all weak references to an object to null on destroy). However, that would be harder to use, harder to implement implement and less efficient.

Objectice-C has associated objects, which are a mix between Hashmaps and what is suggested here: https://medium.com/@kostiakoval/objective-c-associated-objects-8896854c681b

@Cokemonkey11
Copy link
Collaborator

I wonder if you can implement this as a library using WeakReference. Not a huge fan of that Objective-C feature...

@peq
Copy link
Collaborator

peq commented Nov 15, 2017

With WeakReference you could implement a WeakHashMap that automatically (and lazily) removes entries when the key is destroyed.

With that you could implement the killCount extension for UnitWrapper like this:

var killCount = new WeakHashMap<UnitWrapper, int>()

function UnitWrapper.setKillCount(int kk)
	killCount.put(this, kk)

function UnitWrapper.getKillCount() returns int
	if killCount.has(this)
		return killCount.get(this)
	return 0

If we implement operator overloading for property access (#297), UnitWrapper.killCount could then be used like a normal property.

I think getting rid of more boilerplate code would require macros or a specialized implementation of this feature. Then the code above would just be:

int UnitWrapper.killCount = 0

@Trokkin
Copy link
Author

Trokkin commented Nov 17, 2017

But this could also be translated to what I was talking about, like if you were adding a variable and its initializator to the class, with same functionality, but it would work faster and won't create additional overhead.
I knew I can get it working with hashmaps, I knew I can get it working with 'function.set/getKillCount()', but it is not something I want to use every time I want to attach a value to a class, so I suggested this decision. It would be good to have such a macro @peq suggested, but why should it wrap weakhashmap instead of array with construct hook?
Taking about hooks, I'd started another ticket with it if I weren't thinking it's highly related to "extension variable" feature. I find it very useful in other places as well - what about it?

@peq
Copy link
Collaborator

peq commented Nov 18, 2017

I don't like the concept of hooks. It is like the COMEFROM statement and makes it very hard to read and debug source code.

If you want to call some functions every time a certain class is constructed or destroyed, you can use callFunctionsWithAnnotation.
Put callFunctionsWithAnnotation("@unitwrapperConstructorHook") into the constructor of UnitWrapper and then all functions annotated with @unitwrapperConstructorHook will be called.
You can also use the classical observer pattern, but of course that requires some overhead.

@Trokkin
Copy link
Author

Trokkin commented Nov 19, 2017

Oh, I didn't know about that. Is that compiletime feature? If it is so, then hooks are actually worse than call with annotation... thanks for that. Though I'd like that it's syntax would be shorter.
Yep, as far as I know triggers are pretty much observer pattern, even if it lacks some functionality that require additional overhead.

@IgorSamurovic
Copy link

IgorSamurovic commented Dec 27, 2017

Guys, what about this?

class UnitWrapper // base class, with the constructor that'll be called and all that
    protected unit wrapped

    construct(Unit u)
        this.wrapped = u

    ondestroy
        // does something

partial class UnitWrapper // can only be instantiated from the base class
    protected Inventory inventory

    construct() // called when the base class is initialized
        inventory = new Inventory(this.wrapper)

    ondestroy
        destroy inventory

I find this concept to be fairly potent in C#, and in general it is much better for wc3 than alternatives since it allows you to have a clean set of arrays (since data structure is array-based) instead of instantiating a new object for every component of the main class you'd like to add.

My workaround for this abuses arrays and casting to int, as wells as closures for creation/destruction so it is very ugly. It could be really nice and smooth if this was implemented.

Still though, this can cause people to break down classes into partial classes that should really be in separate classes, so there can be some concern that people will misuse this, but even then it'd be a cool feature in a way - being able to extend data (and not only methods) would be cool.

The main reason why I want this isn't even the data attachment, it's the seamless control over creation/destruction.

This could be an alternative:

class UnitWrapper
    use UnitWrapperInventory // explicit, so that the module isn't randomly added, more control
          over what is and what isn't in the class

    protected unit wrapped

    construct(Unit u)
        this.wrapped = u

    ondestroy
        // does something

module UnitWrapperInventory extends UnitWrapper
    protected Inventory inventory

    construct()
        inventory = new Inventory(this.wrapper)

    ondestroy
        destroy inventory

@Trokkin
Copy link
Author

Trokkin commented Dec 28, 2017

Seems really good.
A notice about hooks: I'm looking for a way to implement UnitMoveSpeedX without the need of replacing every call of SetUnitMoveSpeed native with system's own function. Hooks would be a nice feature to have in this case, when there's a system that extends some native functionality.

@IgorSamurovic
Copy link

For all the effort that went into a design to move things in wurst away from the global scope, you want a feature that essentially works in the global scope. And what you want to accomplish can be solved with one mass replace. When you see a function SetUnitMoveSpeed, you need to know what it does. It is a native, so you should be familiar with it. If it is hooked to do some random thing somewhere else, then you no longer know what it does, and nobody reading your code should be expected to randomly know it is hooked to something else somewhere else.

Even partial classes suffer from this problem to a degree, which is why they are to be applied only in specific circumstances - when you know where all the parts are, and they mostly affect the class' scope, not the global scope, aside from when you actually use the extension methods, of course.

@wurstscript wurstscript locked and limited conversation to collaborators Aug 10, 2021
@peq peq closed this as completed Aug 10, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

5 participants