Skip to content

Add hook and extension variables features #571

Description

@Trokkin

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.

Activity

  1. Cokemonkey11 commented on Nov 13, 2017

    @Cokemonkey11
    Collaborator

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

  2. Trokkin commented on Nov 13, 2017

    @Trokkin
    Author

    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.

  3. Cokemonkey11 commented on Nov 14, 2017

    @Cokemonkey11
    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?

  4. peq commented on Nov 14, 2017

    @peq
    Collaborator
  5. Cokemonkey11 commented on Nov 14, 2017

    @Cokemonkey11
    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.

  6. peq commented on Nov 14, 2017

    @peq
    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.

    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

  7. Cokemonkey11 commented on Nov 15, 2017

    @Cokemonkey11
    Collaborator

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

  8. peq commented on Nov 15, 2017

    @peq
    Collaborator

    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
    
  9. Trokkin commented on Nov 17, 2017

    @Trokkin
    Author

    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?

  10. peq commented on Nov 18, 2017

    @peq
    Collaborator

    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.

  11. Trokkin commented on Nov 19, 2017

    @Trokkin
    Author

    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.

  12. IgorSamurovic commented on Dec 27, 2017

    @IgorSamurovic

    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
    
  13. Trokkin commented on Dec 28, 2017

    @Trokkin
    Author

    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.

  14. IgorSamurovic commented on Dec 28, 2017

    @IgorSamurovic

    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.

  15. locked and limited conversation to collaborators on Aug 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions