Repository navigation
Add hook and extension variables features #571
Description
Activity
I'm not sure I understand what extension variables are. Can you point out what this is in other programming languages?
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.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?
- Everything can be seen as a category, but I don't think category theory is helpful here. I don't want users of Wurst to think about category theory and I don't want abstractions in Wurst that nobody understands. Then we will end at quotes like:All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor.I think it is worth to add the extension-attributes feature to the language directly. It adds some benefit, because currently it is not possible to initialize the value, when an object is created (and it saves 4 lines of code compared to an array with getter and setter extension methods). The feature can be compared to partial classes in C#, so it is not completely new. Most languages do not have this features, because it makes it harder to implement separate compilation, but for Wurst/Wc3 that is not such a big problem.
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.
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
WeakReferenceto handle this together withGC. We could also add something likeWeakReference(automatically setting all weak references to an object tonullon 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
I wonder if you can implement this as a library using WeakReference. Not a huge fan of that Objective-C feature...
With
WeakReferenceyou could implement aWeakHashMapthat automatically (and lazily) removes entries when the key is destroyed.With that you could implement the
killCountextension forUnitWrapperlike 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 0If we implement operator overloading for property access (#297),
UnitWrapper.killCountcould 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 = 0But 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?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.
PutcallFunctionsWithAnnotation("@unitwrapperConstructorHook")into the constructor ofUnitWrapperand then all functions annotated with@unitwrapperConstructorHookwill be called.
You can also use the classical observer pattern, but of course that requires some overhead.Reacted by Cokemonkey11 and Vladislav BogdanovOh, 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.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 inventoryI 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 inventoryReacted by Vladislav BogdanovSeems 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.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.
Reacted by Vladislav Bogdanov- locked and limited conversation to collaborators
on Aug 10, 2021
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 += 1as an example. I think that their initialization can be hooked to construct and onDestroy (likehook UnitWrapper.construct /n/t killCount = 0for my example). And I mean they're restricted to class types only.Probably they could be allowed only with special flags like
hookableandextensible.