• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Linked List

Status
Not open for further replies.

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
With the addition of Lua and its rising there's quite a few systems that need to be remade in such scripting language.
Linked list is one of the few subjects I lack knowledge on, might I say, pretty noobish, however I want to get somewhat deeper into it.

Lua:
do
    list = {}
    function list.create()
        local list = {}
        list.first = nil
        list.last = nil
        list.count = 0
        return list
    end
    ---Insert an element after the last node
    function list.push(list, value)
        local item = {}
        item.value = value
        --Current empty queue
        if list.first == nil then
            list.first = item
            list.last = item
            list.count = 1
        else 
            --Non empty queue, add to end
            list.last.next = item
            item.pre = list.last
            list.last = item
            list.count = list.count + 1
        end
    end
    ---Insert an element to the beginning
    function list.insert(list, value)
        local item = {}
        item.value = value
        if list.first == nil then
            list.first = item
            list.last = item
            list.count = list.count + 1
        else
            item.next = list.first
            list.first.pre = item
            list.first = item
            list.count = list.count + 1
        end
    end
    function list.removeFirst(list)
        local val = nil
        --Empty stack
        if list.count == 0 then
        --Only one element
        elseif list.count == 1 then
            val = list.first.value
            list.first = nil
            list.last = nil
            list.count = 0
        --Multiple elements, first with value
        elseif list.first ~= nil then
            --Value
            val = list.first.value
            --Head to second
            list.first = list.first.next
            --Nonempty
            if list.first ~= nil then
                list.first.pre = nil
                list.count = list.count - 1
            else
                list.count = 0
            end
        end
        return val
    end
    ---Delete last element
    function list.removeLast(list)
        local item = list.last
        if list.last ~= nil then
            --If there is only one element in itself
            if list.last.pre == nil then
                list.first = nil
                list.last = nil
                list.count = 0
            else
                --More than one element
                list.last = list.last.pre
                list.last.next = nil
                list.count = list.count - 1
            end
        end
        if item ~= nil then
            return item.value
        end
    end
    
    --looping through the whole list
    function list.foreach(list, func)
        local temp = list.first
        local continue = true
        while temp ~= nil and continue do
            continue = func(temp.value)
            if not continue then
                break
            end
            temp = temp.next
        end
    end
end
The code above can be found on this link https://developpaper.com/lua-implementation-of-linked-list/
I modified it a bit for my own purposes, but it seems to work (didn't test everything).
From what I understand, this is a doubly linked list, I'm not sure if it's optimized or if it's the best to use in some cases where linked lists are necessary.

There's also another one, a circular doubly linked list: LinkedList.lua

Any thoughts, comments about this use and usefulness as a later worthy submission?
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
Linked lists are useful when you want to quickly remove and append elements with the only lookup being iteration. All these operations are executed with O(1) complexity. If you want to lookup a specific element at a position of the list then linked lists are not that good as they have O(n) complexity.

This is in contrast with the inbuilt array list support of Lua tables where they have O(1) lookup both positionally and iteratively but element removal is O(n) with exception for tail element.

Due to the real time nature of Warcraft III you generally want to avoid generating garbage to be collected, especially with code that might be used frequently. As such recycling linked list node tables ("item" in the above code example) is a potential optimisation to reduce garbage collector overhead.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Linked lists are useful when you want to quickly remove and append elements with the only lookup being iteration. All these operations are executed with O(1) complexity. If you want to lookup a specific element at a position of the list then linked lists are not that good as they have O(n) complexity.

This is in contrast with the inbuilt array list support of Lua tables where they have O(1) lookup both positionally and iteratively but element removal is O(n) with exception for tail element.

Due to the real time nature of Warcraft III you generally want to avoid generating garbage to be collected, especially with code that might be used frequently. As such recycling linked list node tables ("item" in the above code example) is a potential optimisation to reduce garbage collector overhead.
I see, so I guess it's ok to post both of these linked lists on code submissions (obviously with the source link where it was taken from)? I'll modify it slightly for some conviniences.
 
Level 20
Joined
Jul 10, 2009
Messages
478
One feature I'm missing on these implementations is the ability to insert an item directly before or after an existing list element.
Linked lists can do that in O(1), which is one of the advantages that I consider useful to me.

If I only needed the simple case presented in the code (only able to append items on top and bottom of the list), I could even use a normal Lua table instead, where indices count upwards on the top end and downwards on the bottom end (so you basically save the topmost and bottommost index as well as all list elements in one table). Yes, the bottommost index can be negative.

Keep in mind that Lua tables are so flexible that Linked Lists are just less likely to be used than in JASS. At least, I haven't needed a single one in the last year of development.
Still, having such a resource on hive is a good thing and many people might find good use for it. :)

So I'd aim for a resource that fits for more use cases than the basic implementation.
Things to consider:
  • insertAfter, insertBefore
  • remove (for specific item that is anywhere in the list)
  • onInsert/onRemove hooks
  • getSize (although that requires O(n) operations, but can still be useful)
  • clear

Best regards
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
If I only needed the simple case presented in the code (only able to append items on top and bottom of the list), I could even use a normal Lua table instead, where indices count upwards on the top end and downwards on the bottom end (so you basically save the topmost and bottommost index as well as all list elements in one table). Yes, the bottommost index can be negative.
Lua tables in sequence (list) form are only designed to operate starting from positive numeric index 1 and going up without holes. Indices below numeric value 1 are not considered part of the sequence. Indices beyond a hole might or might not be counted for list related operations and stored in the array due to disrupting the sequence.

This limitation has performance implications since Lua will usually store the sequence part of a table in a dynamic array while every other mapping is likely to be stored in a hash table. This means lookup and assignment operations within a sequence are computationally less intensive. Sequences also do not store the key part of a mapping so are more compact in memory.

This is assuming Lua 5 is used. Given that Lua 5 was released 20 years ago and even predates Warcraft III, I would hope Reforged uses it.

Technical reference that explains the mechanic in section 4...
insertAfter, insertBefore
The problem with these is that they are both O(n) operations if performed outside an iterator as you need to iterate through the list to find the position before you can insert with O(1) complexity anyway. As part of an iterator you can use the O(n) nature of the iterator to skip the search and insert relative to the current position with O(1). This can be useful in some systems where you have a set of data, such as ability instances, which you are iterating through to update and then want to remove one, such as due to expiring, before continuing to iterate.
getSize (although that requires O(n) operations, but can still be useful)
The linked list object itself (not the nodes) can track the length of the list as an integer that is updated after every insert or remove operation. This results in a complexity of O(1) without impacting complexity elsewhere.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Lua tables in sequence (list) form are only designed to operate starting from positive numeric index 1 and going up without holes. Indices below numeric value 1 are not considered part of the sequence. Indices beyond a hole might or might not be counted for list related operations and stored in the array due to disrupting the sequence.
True, but I wasn't talking about sequences, so what's your point here? :)
In general, Lua tables can hold any numeric key and you can easily loop from the bottommost to the topmost integer key with a simple for-loop (as long as you know these two keys). Nil-elements are not problematic as long as you don't use ipairs, #-operator, etc.
Keep in mind that table.pack is using the same method to allow nil elements (save topmost index to allow for-loop).
Example of the one-table-based-List I was talking about:
The list below is static and I was too lazy to include remove-methods, but it shows what I've been talking about.
Please note that this is not a suggestion on what to submit in the Code subforums, but I rather want to show, what logic I could easily use to avoid a dedicated LinkedList-library, when I only needed the standard operations (insert and remove on top/bottom).
Lua:
List = {
    top = 1
    ,   bottom = 0
}

function List.pushTop(newItem)
    List[List.top] = newItem
    List.top = List.top + 1
end

function List.pushBottom(newItem)
    List[List.bottom] = newItem
    List.bottom = List.bottom - 1
end

function List.size()
    return List.top - List.bottom - 1
end

--you can loop this way
for i = List.bottom + 1, List.top - 1 do
    doSomething(List[i]) --List[i] can be nil, not an issue
end
This limitation has performance implications since Lua will usually store the sequence part of a table in a dynamic array while every other mapping is likely to be stored in a hash table.
Again, what is your point here? Do you think that an implementation using the hashtable part should be avoided?
Yes my example is using keys outside array range, which will lead to the Lua engine saving values in the hashtable part instead of the array part (definitely for the 0-and-below-keys, potentially for the positive keys, if you have inserted too many nil values).
But the above implementation will still have a better performance than the standard Linked List, because it doesn't need the {}-wrap for every element.
Keep in mind that item.value from the standard Linked List also implies a hashtable lookup on every read access. The above example saves that overhead.
In the end, none of these implementations would have a performance impact critical enough for even being worth a discussion...

The problem with these is that they are both O(n) operations if performed outside an iterator as you need to iterate through the list to find the position before you can insert with O(1) complexity anyway. As part of an iterator you can use the O(n) nature of the iterator to skip the search and insert relative to the current position with O(1). This can be useful in some systems where you have a set of data, such as ability instances, which you are iterating through to update and then want to remove one, such as due to expiring, before continuing to iterate.
To clarify, insertBefore(listIndex, newItem) would be O(n), but I was proposing insertBefore(existingItem, newItem), which is O(1).
There are enough scenarios, where you already know the exact item that you want to preceed with a new item and where you don't need to search again.
Again, I'm assuming a static list to simplify my point. Non-static lists would use :-notation and self-keyword instead.
Lua:
function List.insertBefore(existingItem, newItem)
    --potentially need to wrap newItem into {}, depending on the implementation.
    --update List object properties (if there is a list object)
    if List.first == existingItem then
        List.first = newItem
    end
    List.count = List.count + 1
    --update item connections
    existingItem.prev.next = newItem
    newItem.prev = existingItem.prev
    newItem.next = existingItem
    existingItem.prev = newItem
    --maybe return newItem
end
The linked list object itself (not the nodes) can track the length of the list as an integer that is updated after every insert or remove operation. This results in a complexity of O(1) without impacting complexity elsewhere.
Yes, you can do that, if your implementation has a "List object" or a "root node" to store such information. Some implementations use loosely connected items, though.
This is assuming Lua 5 is used. Given that Lua 5 was released 20 years ago and even predates Warcraft III, I would hope Reforged uses it.
Warcraft Reforged is using Lua 5.3.
 
Last edited:

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
Again, what is your point here? Do you think that an implementation using the hashtable part should be avoided?
For lists it generally should be. The sequence part will perform significantly better.

Keeping as a sequence also allows use of the standard API to handle the list, which will likely perform better than any custom code on smaller lists even with O(n) complexity on some of the operations.
 
Level 20
Joined
Jul 10, 2009
Messages
478
The LinkedList implementation from the main post is using hashtable operations, I presented a feature-equivalent alternative having less hashtable operations and you are criticising that alternative for still having more than zero of them?

Yes, you can play around a bit with indices, like keeping the top part of the chain in array slots with even numbers and the bottom part in array slots with odd numbers, keeping everything in the array part, but I was comparing my implementation to a proper Linked List, which doesn't exist purely based on a single sequence.

So without any offensive intention, I'm unable to understand your point currently. Happy to hear your explanation, though.

To be more clear, the main post implementation creates a table for every item insertion along with item.next, item.pre, etc. properties. All of these are operations performed in the hashtable part.
My alternative doesn't create tables upon insertion and only needs two hashtable lookups per insertion instead of five to six.

Keeping as a sequence also allows use of the standard API to handle the list, which will likely perform better than any custom code on smaller lists even with O(n) complexity on some of the operations.
I don't see much benefit of using the standard table API for building a LinkedList (/alternative). Many parts of the API can even be implemented in a much better way when specialized on the use case, table.insert and the #operator being good examples for that.

But anyway, I wasn't going to sell that code for anything.
Just wanted to show that Lua tables are so flexible already that a basic implementation of LinkedLists wouldn't catch me enough to use it (because replacing it by an alternative is so easy), so for a real LinkedList submission on Hive I'd recommend to have a broader set of features (see my first post).
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
After some pain, I've somewhat merged the main code with some ideas of the code from the second link. Keep in mind that it's still incomplete.
Lua:
-- Circular, doubly linked list --
----------------------------------
do
    LinkedList = {}
    -- Creates a list from table or variable n of values
    function LinkedList.createEx(...)
        local args = {...}
        local list = {}
        local item = newNode(args[1])
        list.first = item
        list.last = item
        for i = 2, #args do
            add(list, args[i])
        end
        args = nil
        return list
    end
 
    local function newNode(val)
        local node = {}
        node.value = val
        node.next = node
        node.prev = node
        return node
    end
    local function add(list, val)
        local item = newNode(val)
        item.next = list.last.next
        list.last.next = item
        item.prev = list.last
        list.last = item
        item.next.prev = item
        list.count = list.count + 1
    end
    -- Inserts a node after the given node and returns a reference.
    function LinkedList.insert(list, node, val)
        local item = {}
        item.value = val
        item.next = node.next
        item.prev = node
        node.next = item
        item.next.prev = item
        --update if given node was last of the list.
        if node == list.last then
            list.last = item
        end
        list.count = list.count + 1
        return item
    end
    -- Inserts a node before first node node on the list.
    function LinkedList.insertBefore(list, val)
        local item = {}
        item.value = val
        item.next = list.first
        item.prev = list.last
        item.next.prev = item
        list.first = item
        list.last.next = item
    end
    -- Remove a node of the list, return reference to next node --
    function LinkedList.erase(node)
        local index = node.next
        node.prev.next = node.next
        node.next.prev = node.prev
        node = nil
        return index
    end
    -- Print the whole list, avoiding circularity --
    function LinkedList.print(node)
        local it = node
        repeat
            print(it.value)
            it = it.next
        until it == node
    end
end
Then I attempted to test with this code:
Lua:
function test()
    local t = CreateTrigger()
    TriggerRegisterTimerEventSingle(t, 2.00)
    TriggerAddAction(t, function()
            try(function()
            local l = LinkedList.createEx(1, 2, 3)
            --LinkedList.print(l.first)



            --list.push(l, "a")
            --list.push(l, "d")
            --list.push(l, "g")
            --list.foreach(l, loop)
            end)

    end)
end

onInit(test)
and I still got an error. It refers to this line: local item = newNode(args[1])
I don't really understand why I get this error because, while the function NewNode is indeed local, it is engulfed in a do end block, and so is LinkedList.createEx. So, did I miss something very obvious here or what?
The LinkedList implementation from the main post is using hashtable operations, I presented a feature-equivalent alternative having less hashtable operations and you are criticising that alternative for still having more than zero of them?

Yes, you can play around a bit with indices, like keeping the top part of the chain in array slots with even numbers and the bottom part in array slots with odd numbers, keeping everything in the array part, but I was comparing my implementation to a proper Linked List, which doesn't exist purely based on a single sequence.

So without any offensive intention, I'm unable to understand your point currently. Happy to hear your explanation, though.

To be more clear, the main post implementation creates a table for every item insertion along with item.next, item.pre, etc. properties. All of these are operations performed in the hashtable part.
My alternative doesn't create tables upon insertion and only needs two hashtable lookups per insertion instead of five to six.


I don't see much benefit of using the standard table API for building a LinkedList (/alternative). Many parts of the API can even be implemented in a much better way when specialized on the use case, table.insert and the #operator being good examples for that.

But anyway, I wasn't going to sell that code for anything.
Just wanted to show that Lua tables are so flexible already that a basic implementation of LinkedLists wouldn't catch me enough to use it (because replacing it by an alternative is so easy), so for a real LinkedList submission on Hive I'd recommend to have a broader set of features (see my first post).
Honestly the index based operations in a linkedlist sound very weird to me.
But your ideas on your first post are definetely on point.
 

Attachments

  • error.png
    error.png
    314.1 KB · Views: 19
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Honestly the index based operations in a linkedlist sound very weird to me.
Haha :D
Well, it wasn't really a linked list, but a feature-equivalent index-based alternative using a single table, showing that Lua tables are flexible enough to provide easy replacement for simple Linked List use cases.
It refers to this line: local item = newNode(args[1])
The newNode function is local itself, so it has to be defined above the createEx-function (otherwise the createEx function tries to gather newNode from global scope).
You can also just write local newNode before createEx and leave its function definition below createEx (but in that case, remove the local keyword from the function definition).
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Haha :D
Well, it wasn't really a linked list, but a feature-equivalent index-based alternative using a single table, showing that Lua tables are flexible enough to provide easy replacement for simple Linked List use cases.
Yeah, lua tables are very powerful themselves. However, like you said, there are still some usecases for Linked List, that's what I'm aiming at too :D
The newNode function is local itself, so it has to be defined above the createEx-function (otherwise the createEx function tries to gather newNode from global scope).
You can also just write local newNode before createEx and leave its function definition below createEx (but in that case, remove the local keyword from the function definition).
Yeah, I figured after trial and error, and then a bit of googling. It does make sense, but what's still bothering me is that the one case I tested it by putting newNode above createEx but AFTER the LinkedList table being initialized. So this didn't work:
Lua:
do
    LinkedList = {}

    local function newNode(val)--WHY DON'T YOU WORK????
        local node = {}
        node.value = val
        node.next = node
        node.prev = node
        return node
    end
   
    -- Creates a list from table or variable n of values
    function LinkedList.createEx(...)
        local args = {...}
        local list = {}
        local item = newNode(args[1])
        list.first = item
        list.last = item
        list.count = 1
        for i = 2, #args do
            add(list, args[i])
        end
        args = nil
        return list
    end
--blablablabla
end
So yeah, had to put it above LinkedList table and then it finally worked, that's still mindboggling me.
 
Level 20
Joined
Jul 10, 2009
Messages
478
Yeah, lua tables are very powerful themselves. However, like you said, there are still some usecases for Linked List, that's what I'm aiming at too :D
Absolutely, thanks for doing the effort!
I just felt pressured to elaborate because Dr Super Good challenged me so hard :D
but what's still bothering me is that the one case I tested it by putting newNode above createEx but AFTER the LinkedList table being initialized. So this didn't work:
Your example code is using the add-function in the for i = 2, #args do loop, but that function is not defined.
From a Lua perspective, "add" is nil, so add(x) is nil(x) and therefore you get another "Attempt to call a nil value"-error.
That particular error I personally find very hard to debug, but searching for undefined function names is mostly the way to go here.
Just by chance, are you also using setmetatable(_G,{__index=function(_, n) print("Trying to read undeclared global : "..tostring(n)) end,}) in your test environment? That line would have printed "Trying to read undeclared global: add". I know, the line can be annoying, if you intentionally nil variables in global scope.

Edit: Btw, VS Code with the sumneko extension has great color coding (grey for globals, blue or yellow for locals) to show you the scope, where a name is taken from. Also, it underlines names in red, when it has not been defined within the used scope anywhere else.
That feature alone helps me so much that I rarely run into scope issues.
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Just by chance, are you also using setmetatable(_G,{__index=function(_, n) print("Trying to read undeclared global : "..tostring(n)) end,}) in your test environment? That line would have printed "Trying to read undeclared global: add". I know, the line can be annoying, if you intentionally nil variables in global scope.
Actually, I'm not. However, I'm using it on my other maps. I didn't think it would necessary to import that to a "dummy" map where I'm testing this stuff out, despite having a very short version of your debug library, the "try" function that uses xpcall :p
Edit: Btw, VS Code with the sumneko extension has great color coding (grey for globals, blue or yellow for locals) to show you the scope, where a name is taken from. Also, it underlines names in red, when it has not been defined within the used scope anywhere else.
That feature alone helps me so much that I rarely run into scope issues.
I had installed 3 extensions: lua from keyring, vscode-lua from trixnz and lua from sumneko. For some reason i had sumneko's disabled and I think it was because it doesn't recognise the native functions of wc3. E.g. "
GetUnitsOfPlayerAndTypeId(whichPlayer, unitid)". And I had to mark them as a defined global so it would stop giving me errors.
But yes, I tested it and it does help with the scope issues.
 
Level 20
Joined
Jul 10, 2009
Messages
478
For some reason i had sumneko's disabled and I think it was because it doesn't recognise the native functions of wc3.
The main post of Lua Debug Utils also has Lua definition files for all Wc3 functions from common.lua and blizzard.lua attached.
Keeping those in your workspace will let the sumneko extension recognise all natives and even support auto-completion, which is super convenient. :)
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Ok, I'm not going to lie, I suck at this workspace stuff. I've also looked at AGD's case and still don't understand.
It's still not recognising for me.
Here's what i did:
1. opened VS studio, and then open my main VS code folder, which contains 2 folders, jass and lua.
2. add a folder which contains common.lua.txt and blizzard.lua.txt to the workspace.
3. save the workspace
4. restart VS studio
5. profit??? guess not :(
So I'm still pretty confused as to why it didn't work.
 
Level 20
Joined
Jul 10, 2009
Messages
478
Could you try the following and see, if any of them work:

1. Move both common.lua.txt and blizzard.lua.txt inside the Lua folder
2. Cut the ".txt" ending from their filename
3. Downgrade your sumneko extension to version 2.3.7 (very stable version that I like to use)

Also keep in mind that loading all definitions from the files might take the extension a few minutes. It should show that it's still loading during that time, though.

Happy to mention the solution in the Debug Utils post, if it works.
 
Level 20
Joined
Jul 10, 2009
Messages
478
Perfect, thanks for the feedback :)

I reckon the .txt-ending let to VSCode not treating the files as Lua-files and the sumneko extension only scans Lua-files.

I had to append the .txt-ending to make the files uploadable to Hive, but I've added the information that it needs to be cut to the main post of Debug Utils now. :)
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Hi, again.
I've already had this done sometime ago, but I got lazy from posting it here.
I think it is complete now.
Lua:
--[[    
            Circular, doubly linked list
                Lua version by Wrda

*****************************************************************************
* A script that allows the possibility to create a sequence of any object   *
* linked together.                                                          *
*****************************************************************************

------------------------------------------------------------------------------
* API:
*
*   LinkedList.create(...) -> table
*       - Creates a new linked list with specified arguments as values.
*       - Example:
*           local l = LinkedList.Create(2, "John", GetTriggerUnit(), Player(0)) -- a linked list with 4 elements
*   LinkedList.insert(table list, table node, any val) -> table
*       - Inserts a node after the given node and returns it as a reference.
*   LinkedList.insertBefore(table list, any val)
*       - Inserts a node before the first node on the list.
*   LinkedList.erase(table list, table node) -> table
*       - Remove a node of the list, return reference to the next node.
*   LinkedList.clear(table list)
*       - Clears the linked list, removing all nodes.
*   LinkedList.forEach(table node, function code, ...)
*       - Iterates through the whole list, avoiding circularity.
*       - Starts from node position, using its value.
*       - code must have at least one parameter, which is each node's value.
*       - Example:
*           LinkedList.forEach(l.last.prev, function(v) print(v) end)
*   LinkedList.onInsert(list, code)
*       - Fires when a node is inserted on the list.
*   LinkedList.onErase(list, code)
*       - Fires when a node is erased on the list.
*-------------------------------------------------------------------------------
* Members:
*
*   next        -> the next node of the node mentioned in the list. E.g. <node>.next
*   prev        -> the previous node of the node mentioned in the list. E.g. <node>.prev
*   first       -> the first node of the list. E.g. <listname>.first
*   last        -> the last node of the list. E.g. <listname>.last
*   count       -> the amount of nodes in the list. E.g. <listname>.count
*   onInsert    -> the function that fires upon node insert on the list. e.g. <listname>.Insert
*   onErase     -> the function that fires upon node erase on the list. e.g. <listname>.Erase
*--------------------------------------------------------------------------------
]]

do
    LinkedList = {}

    local function newNode(val)
        local node = {}
        node.value = val
        node.next = node
        node.prev = node
        return node
    end

    local function add(list, val)
        local item = newNode(val)
        item.next = list.last.next
        list.last.next = item
        item.prev = list.last
        list.last = item
        item.next.prev = item
        list.count = list.count + 1
    end

    ---Creates a new linked list with specified arguments as values.
    ---@return table
    function LinkedList.create(...)
        local args = {...}
        local list = {}
        local item = newNode(args[1])
        list.first = item
        list.last = item
        list.count = 1
        for i = 2, #args do
            add(list, args[i])
        end
        args = nil
        return list
    end

    ---Inserts a node after the given node and returns it as a reference.
    ---@param list table
    ---@param node table
    ---@param val any
    ---@return table
    function LinkedList.insert(list, node, val)
        local item = {}
        item.value = val
        item.next = node.next
        item.prev = node
        node.next = item
        item.next.prev = item
        --update if given node was last of the list.
        if node == list.last then
            list.last = item
        end
        list.count = list.count + 1
        if list.onInsert then
            list.onInsert()
        end
        return item
    end

    ---Inserts a node before the first node on the list.
    ---@param list table
    ---@param val any
    function LinkedList.insertBefore(list, val)
        local item = {}
        item.value = val
        item.next = list.first
        item.prev = list.last
        item.next.prev = item
        list.first = item
        list.last.next = item
        list.count = list.count + 1
        if list.onInsert then
            list.onInsert()
        end
    end

    ---Remove a node of the list, return reference to the next node.
    ---@param list table
    ---@param node table
    ---@return table
    function LinkedList.erase(list, node)
        local index = node.next
        node.prev.next = node.next
        node.next.prev = node.prev
        --update the edges of the list
        if node == list.first then
            list.first = node.next
        elseif node == list.last then
            list.last = node.prev
        end
        node = nil
        list.count = list.count - 1
        if list.onErase then
            list.onErase()
        end
        return index
    end

    ---Clears the linked list, removing all nodes.
    ---@param list table
    function LinkedList.clear(list)
        list = nil
    end

    ---Iterates through the whole list, avoiding circularity.
    ---Starts from node position, using its value.
    ---code must have at least one parameter, which is each node's value.
    ---@param node table
    ---@param code function
    ---@param ... any
    function LinkedList.forEach(node, code, ...)
        if node then
            local it = node
            repeat
                code(it.value, ...)
                it = it.next
            until it == node
        end
    end

    ---Fires when a node is inserted on the list.
    ---@param list table
    ---@param code function
    function LinkedList.onInsert(list, code)
        list.onInsert = code
    end

    ---Fires when a node is erased on the list.
    ---@param list table
    ---@param code function
    function LinkedList.onErase(list, code)
        list.onErase = code
    end
end
Anything to improve?
I guess I could turn this into a submission on lua code section of the forum :p
 
Level 20
Joined
Jul 10, 2009
Messages
478
Really good to see that you are documenting properly and using emmy annotation, much appreciated :)

Emmy annotation even offers some additional features that you might want to consider:
---@class <classname> declares a new object type that you can use in ---@param and the likes.
---@vararg <type> is used to annotate the type of all arguments specified with the "..."-operator
---@param <name> fun(<paramName>:<paramType>):<returnType> declares a function argument together with its input and output parameters

Especially the return values can be specified much more clearly, if you have declared classes for the LinkedList and LinkedListNode before.

In your example, it could look like this:
Lua:
    ---@class LinkedListNode
    LinkedListNode = {
        next = nil      ---@type LinkedListNode the next node in the list
        ,   prev = nil  ---@type LinkedListNode the previous node in the list
        ,   value = nil ---@type any the value contained in this node
    }

    ---@class LinkedList
    LinkedList = {
        first = nil         ---@type LinkedListNode
        ,   last = nil      ---@type LinkedListNode
        ,   count = 0       ---@type integer
        ,   onInsert = nil  ---@type fun(insertedNode:LinkedListNode)
        ,   onErase = nil   ---@type fun(erasedNode:LinkedListNode)
    }

    ---Creates a new linked list with specified arguments as values.
    ---@vararg any elementsToAdd
    ---@return LinkedList
    function LinkedList.create(...)
    end

    ---Inserts a node after the given node and returns it as a reference.
    ---@param list LinkedList
    ---@param node LinkedListNode
    ---@param val any
    ---@return LinkedListNode
    function LinkedList.insert(list, node, val)
    end

    ---Iterates through the whole list, avoiding circularity.
    ---Starts from node position, using its value.
    ---code must have at least one parameter, which is each node's value.
    ---@param loopNode LinkedListNode
    ---@param code fun(loopNode:LinkedListNode, ...)
    ---@vararg any arguments given to code after loopNode
    function LinkedList.forEach(node, code, ...)
    end

    ---Fires when a node is inserted on the list.
    ---@param list LinkedList
    ---@param code fun(insertedNode:LinkedListNode)
    function LinkedList.onInsert(list, code)
    end

Skimming through your code also got me the following remarks:
  1. Doubly Linked Lists usually have methods specifically for inserting and removing elements at front or back as well as to insert a new element before or after a given node.
    I'd expect insertBefore/insertAfter to insert a new value before/after an existing node and insertFront/insertBack to insert elements to the front/back of the list. I find the current naming a bit misleading. Same logic for remove-methods (I think removeFront and removeBack are missing anyway?)
  2. I'd prefer to have object oriented Lua-notation, i.e. I'd like to write l:clear() instead of LinkedList.clear(l). These two notations don't contradict each other (everyone can still use what he or she prefers), but for the :-notation to work, you would need to add the following code:
    • add setmetatable(list,LinkedList) to LinkedList.create
    • add LinkedList.__index = LinkedList below LinkedList = {}.
    And well, if you do so, it would be more intuitive to use the :-notation also for the method-definitions from a documentation-perspective.
  3. Likewise, I'd prefer to have a generic for-loop over LinkedLists instead of the forEach-method you provided. Generic for-loops are the gold standard for Lua containers, i.e. the possibility to write
    Lua:
    for node in list:elements() do --you can choose a different name for the loop method, but "elements" seems intuitive to me
        print(node.value)
    end
    Also, accessing the loopNode should be possible within the loop (i.e. it should loop over nodes, not values) to allow for removing/inserting nodes within the loop. That would require to clarify the specific behaviour that would come from doing so to the user (like if inserted nodes would immediately join the loop or not).
    Having an additional loop-method for the values is also fine, but don't skip the node-one ;)
  4. In case you want to keep your current forEach-method, please consider taking the list object instead of a node and just looping from front to back. Starting from a certain node is also a fine option to provide, but should be optional for the user.
  5. The create-function currently assumes that you are inserting at least one element. I think, one should be able to create an empty Linked List. Also not exactly sure, if your erase-method accounts for the case, where the resulting list is empty (it looks like it does, but just wanted to be sure).
  6. Using args = table.pack(...) and for i = 2, args.n instead of args = {...} and for i = 2, #args would be the more stable option in case a user intentionally or accidently adds nil-elements to the List. Your create method currently ignores all values beyond the first nil, so LinkedList.create(a,b,c) can lead to unwanted behaviour, if b is nil.
  7. I'd expect from the clear-method that it leaves an empty list instead of destroying it completely.
  8. The circular list maintains circular node reference by definition, so a list can currently only be destroyed without leaks, if you remove all elements one by one.
    Id would be wise to provide a destroy-method that clears enough references to make the list garbage collectable. The same holds for the clear-method, which currently leaks all nodes :)
  9. Hooks onInsert and onErase should be able to take the inserted/removed node as their argument. At least I consider doing something with the inserted/removed node the most important use case.
    For that to happen, onErase must also be called before the actual removal instead of afterwards (which should also be stated in the docs).
  10. Using LinkedList.onInsert and LinkedList.onErase currently doesn't allow for adding multiple hooks. The second onInsert will just overwrite the first. I'd prefer to be able to add as many hooks as I want. If your decision was intentional, it should be stated in the docs.
So lots of small stuff, but nothing game breaking.

Thanks for the work so far and have a good time ;)
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Really good to see that you are documenting properly and using emmy annotation, much appreciated :)

Emmy annotation even offers some additional features that you might want to consider:
---@class <classname> declares a new object type that you can use in ---@param and the likes.
---@vararg <type> is used to annotate the type of all arguments specified with the "..."-operator
---@param fun(<paramName>:<paramType>):<returnType> declares a function argument together with its input and output parameters

Especially the return values can be specified much more clearly, if you have declared classes for the LinkedList and LinkedListNode before.

In your example, it could look like this:
Lua:
    ---@class LinkedListNode
    LinkedListNode = {
        next = nil      ---@type LinkedListNode the next node in the list
        ,   prev = nil  ---@type LinkedListNode the previous node in the list
        ,   value = nil ---@type any the value contained in this node
    }

    ---@class LinkedList
    LinkedList = {
        first = nil         ---@type LinkedListNode
        ,   last = nil      ---@type LinkedListNode
        ,   count = 0       ---@type integer
        ,   onInsert = nil  ---@type fun(insertedNode:LinkedListNode)
        ,   onErase = nil   ---@type fun(erasedNode:LinkedListNode)
    }

    ---Creates a new linked list with specified arguments as values.
    ---@vararg any elementsToAdd
    ---@return LinkedList
    function LinkedList.create(...)
    end

    ---Inserts a node after the given node and returns it as a reference.
    ---@param list LinkedList
    ---@param node LinkedListNode
    ---@param val any
    ---@return LinkedListNode
    function LinkedList.insert(list, node, val)
    end

    ---Iterates through the whole list, avoiding circularity.
    ---Starts from node position, using its value.
    ---code must have at least one parameter, which is each node's value.
    ---@param loopNode LinkedListNode
    ---@param code fun(loopNode:LinkedListNode, ...)
    ---@vararg any arguments given to code after loopNode
    function LinkedList.forEach(node, code, ...)
    end

    ---Fires when a node is inserted on the list.
    ---@param list LinkedList
    ---@param code fun(insertedNode:LinkedListNode)
    function LinkedList.onInsert(list, code)
    end
Wow, that's epic!
Skimming through your code also got me the following remarks:
  1. Doubly Linked Lists usually have methods specifically for inserting and removing elements at front or back as well as to insert a new element before or after a given node.
    I'd expect insertBefore/insertAfter to insert a new value before/after an existing node and insertFront/insertBack to insert elements to the front/back of the list. I find the current naming a bit misleading. Same logic for remove-methods (I think removeFront and removeBack are missing anyway?)
  2. I'd prefer to have object oriented Lua-notation, i.e. I'd like to write l:clear() instead of LinkedList.clear(l). These two notations don't contradict each other (everyone can still use what he or she prefers), but for the :-notation to work, you would need to add the following code:
    • add setmetatable(list,LinkedList) to LinkedList.create
    • add LinkedList.__index = LinkedList below LinkedList = {}.
    And well, if you do so, it would be more intuitive to use the :-notation also for the method-definitions from a documentation-perspective.
  3. Likewise, I'd prefer to have a generic for-loop over LinkedLists instead of the forEach-method you provided. Generic for-loops are the gold standard for Lua containers, i.e. the possibility to write
    Lua:
    for node in list:elements() do --you can choose a different name for the loop method, but "elements" seems intuitive to me
        print(node.value)
    end
    Also, accessing the loopNode should be possible within the loop (i.e. it should loop over nodes, not values) to allow for removing/inserting nodes within the loop. That would require to clarify the specific behaviour that would come from doing so to the user (like if inserted nodes would immediately join the loop or not).
    Having an additional loop-method for the values is also fine, but don't skip the node-one ;)
  4. In case you want to keep your current forEach-method, please consider taking the list object instead of a node and just looping from front to back. Starting from a certain node is also a fine option to provide, but should be optional for the user.
  5. The create-function currently assumes that you are inserting at least one element. I think, one should be able to create an empty Linked List. Also not exactly sure, if your erase-method accounts for the case, where the resulting list is empty (it looks like it does, but just wanted to be sure).
  6. Using args = table.pack(...) and for i = 2, args.n instead of args = {...} and for i = 2, #args would be the more stable option in case a user intentionally or accidently adds nil-elements to the List. Your create method currently ignores all values beyond the first nil, so LinkedList.create(a,b,c) can lead to unwanted behaviour, if b is nil.
  7. I'd expect from the clear-method that it leaves an empty list instead of destroying it completely.
  8. The circular list maintains circular node reference by definition, so a list can currently only be destroyed without leaks, if you remove all elements one by one.
    Id would be wise to provide a destroy-method that clears enough references to make the list garbage collectable. The same holds for the clear-method, which currently leaks all nodes :)
  9. Hooks onInsert and onErase should be able to take the inserted/removed node as their argument. At least I consider doing something with the inserted/removed node the most important use case.
    For that to happen, onErase must also be called before the actual removal instead of afterwards (which should also be stated in the docs).
  10. Using LinkedList.onInsert and LinkedList.onErase currently doesn't allow for adding multiple hooks. The second onInsert will just overwrite the first. I'd prefer to be able to add as many hooks as I want. If your decision was intentional, it should be stated in the docs.
So lots of small stuff, but nothing game breaking.

Thanks for the work so far and have a good time ;)
1. It is indeed somewhat misleading. Aren't removeFront and removeBack already in erase function? What I mean is, one can simply use LinkedList.erase(l, l.first) or LinkedList.erase(l, l.last) to remove either one of them, since the system always has the references of both of them, I think it will be easier to do this way rather creating another method for something that can be reduced. But yes, insert method is going to be renamed to insertAfter. I think the behavior of insertBefore is pretty easily done with the prev member, so inserting a node before the last node would be insertAfter insertAfter(list, list.last, value). So I guess insertFront would be to insert before the first node of the list, while also becoming the first node.

2. I'm not a big fan of OOP, but can do both options available, of course going to define that in documentation :D

3. Will do!
4. Will do something about it.
5. Yeah, I had thought about that, but for some reason I just skipped it. Currently the erase method still returns a node if there was only one left, have to fix it.
6. You're right.
7. True xd
8. But if the list is the only thing that contains the references, shouldn't nilling the list also nil all elements associated? That's the one part I haven't understood about lua, you need to nil all keys and arrays inside a table first and then the table itself to remove leaks completely?
9. Absolutely.
10. Hm, I don't see the need of so many hooks, so I'll just add that in the docs.

Thanks for the feedback :)
 
Level 20
Joined
Jul 10, 2009
Messages
478
Thanks for the feedback :)
You are welcome :)
8. But if the list is the only thing that contains the references, shouldn't nilling the list also nil all elements associated? That's the one part I haven't understood about lua, you need to nil all keys and arrays inside a table first and then the table itself to remove leaks completely?
You need to remove all references to something in order to make it garbage collectable. That garbage collection can form a chain-reaction, i.e. you typically only need to nil the outmost container (in your case the LinkedList itself).
For example, if you have a table A storing an array B that stores several other values, writing A = nil would remove the reference to the table that we called A before nilling. The garbage collector will collect that table, consequently deleting all entries. One of the entries was the reference to B, so after the first collection cycle, there will be no further reference to B and B will also be collected. Afterwards, the same holds for all elements of B. That logic only applies however, if there are no other objects in your code that still reference A (or B or whatever).

In your case, I was inclined to say that all nodes leak, because they retain references to each other, effectively preventing garbage colelction.
However, I was wrong with that.
Lua seems to be fine with garbage collecting circular references (as long as their is no other outer reference to the objects of course).
Programming in Lua 5.0 said:
Unlike some other collectors, Lua's garbage collector has no problems with cycles. You do not need to take any special action when using cyclic data structures; they are collected like any other data. Nevertheless, sometimes even the smarter collector needs your help. No garbage collector allows you to forget all worries about memory management.
(Link)
I ran a few tests in a dedicated Lua VM (not Wc3) and yes, it looks like all nodes are properly collected once the list is nilled.

So long story short, never mind about this point, it's fine the way you are doing it currently.

10. Hm, I don't see the need of so many hooks, so I'll just add that in the docs.
I think, multiple hooks occur naturally in code, just like adding multiple trigger actions to a single trigger.
By random example, assume that you have created a Linked List of locations that a unit shall move to in a given order.
New locations added to the list would need a mechanism to recognize that the unit has reached it, so you maybe want to create a "unit-comes-in-range-of-this-location"-trigger upon inserting a new location and choose to call LinkedList.onInsert(l, createComesInRangeTrigger).
Now you suddenly also want to paint the future route of the unit in some color on the map and that route needs to be updated upon adding a new location, so you also call LinkedList.onInsert(l, updateVisualRoute) from somewhere else in your code.
Sounds quite reasonable to me, but the current implementation doesn't allow for it.
That's not a deal-breaker for me, though. If you want to stay on one hook max, that's fine, too.
Wrda said:
Absolutely, everything can be done with the existing erase and insertAfter.
I still think, those additional methods would not cause any harm, but just help people to use the List the way they want.
There are many LinkedList implementations out there that just give you the first and last element, so people needing that specific instance of LinkedList would have an easier time using your resource, if you had those methods already defined.
From my personal experience in programming, if you are doing the same thing twice or more in code, better have a function for it (because changing stuff would quickly become tedious and incredibly error prone, if you tried to maintain the same logic at x different places). That means, users would have to add their own method to the library even for such a standard use case. Hence, I'd recommend to have those methods already in the resource. :)
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Ok, so I've been slacking lately and busy with some other stuff, but I'm going to post my "prefinal" state of my resource.
Lua:
--[[    
            Circular, doubly linked list
                Lua version by Wrda
*****************************************************************************
* A script that allows the possibility to create a sequence of any object   *
* linked together.                                                          *
*****************************************************************************
------------------------------------------------------------------------------
* API:
*
*   LinkedList.create(...) -> table
*       - Creates a new linked list with specified arguments as values.
*       - Example:
*           local l = LinkedList.Create(2, "John", GetTriggerUnit(), Player(0)) -- a linked list with 4 elements
*   LinkedList.insert(table list, table node, any val) -> table
*       - Inserts a node after the given node and returns it as a reference.
*   LinkedList.insertBefore(table list, any val)
*       - Inserts a node before the first node on the list.
*   LinkedList.erase(table list, table node) -> table
*       - Remove a node of the list, return reference to the next node.
*   LinkedList.clear(table list)
*       - Clears the linked list, removing all nodes.
*   LinkedList.forEach(table node, function code, ...)
*       - Iterates through the whole list, avoiding circularity.
*       - Starts from node position, using its value.
*       - code must have at least one parameter, which is each node's value.
*       - Example:
*           LinkedList.forEach(l.last.prev, function(v) print(v) end)
*   LinkedList.onInsert(list, code)
*       - Fires when a node is inserted on the list.
*   LinkedList.onErase(list, code)
*       - Fires when a node is erased on the list.
*-------------------------------------------------------------------------------
* Members:
*
*   next        -> the next node of the node mentioned in the list. E.g. <node>.next
*   prev        -> the previous node of the node mentioned in the list. E.g. <node>.prev
*   first       -> the first node of the list. E.g. <listname>.first
*   last        -> the last node of the list. E.g. <listname>.last
*   value       -> the value of a specific node of the list. E.g. <listname>.last.value
*   count       -> the amount of nodes in the list. E.g. <listname>.count
*   onInsertEvents    -> a table array that contains a list of events that fire upon node insertion. #<listname>.onInsertEvents to get its length.
*   onEraseEvents     -> a table array that contains a list of events that fire upon node removal. #<listname>.onEraseEvents to get its length.
*--------------------------------------------------------------------------------
]]
do
    ---@class LinkedList
    LinkedList = {
        first = nil         ---@type LinkedListNode
        ,   last = nil      ---@type LinkedListNode
        ,   count = 0       ---@type integer
        ,   onInsertEvents = nil  ---@type table<integer,fun(insertedNode:LinkedListNode)>
        ,   onEraseEvents = nil   ---@type table<integer,fun(erasedNode:LinkedListNode)>
    }
    LinkedList.__index = LinkedList
    ---@class LinkedListNode
    LinkedListNode = {
        next = nil      ---@type LinkedListNode the next node in the list
        ,   prev = nil  ---@type LinkedListNode the previous node in the list
        ,   value = nil ---@type any the value contained in this node
    }
    local function newNode(val)
        local node = {}
        node.value = val
        node.next = node
        node.prev = node
        return node
    end
    local function add(list, val)
        local item = newNode(val)
        item.next = list.last.next
        list.last.next = item
        item.prev = list.last
        list.last = item
        item.next.prev = item
        list.count = list.count + 1
    end
    ---Creates an empty linked list.
    ---@return LinkedList
    function LinkedList.createEx()
        local list = {}
        setmetatable(list,LinkedList)
        list.count = 0
        list.onInsertEvents = {}
        list.onEraseEvents = {}
        return list
    end
    ---Creates a new linked list with specified arguments as values.
    ---@vararg any elementsToAdd
    ---@return LinkedList
    function LinkedList.create(...)
        local args = table.pack(...)
        local list = {}
        setmetatable(list,LinkedList)
        local item = newNode(args[1])
        list.first = item
        list.last = item
        list.count = 1
        list.eraseCount = 0
        list.onInsertEvents = {}
        list.onEraseEvents = {}
        for i = 2, args.n do
            add(list, args[i])
        end
        args = nil
        return list
    end
    ---Inserts a node after the given node and returns it as a reference.
    ---Only use nil as a node argument to create the first element upon an empty linked list (list.count is 0).
    ---@param list LinkedList
    ---@param node LinkedListNode
    ---@param val any
    ---@return LinkedListNode
    function LinkedList.insertAfter(list, node, val)
        local item = {}
        if list.count == 0 then
            item.value = val
            list.first = item
            list.last = item
            item.next = item
            item.prev = item
            list.count = 1
        else
            item.value = val
            item.next = node.next
            item.prev = node
            node.next = item
            item.next.prev = item
            list.count = list.count + 1
        end
        --update if given node was last of the list.
        if node == list.last then
            list.last = item
        end
        for i, _ in ipairs(list.onInsertEvents) do
            --print(list.onInsertEvents[i](item)
            list.onInsertEvents[i](item)
        end
        return item
    end
    ---Inserts a node before the first node on the list, making it the new head, and returns it as a reference.
    ---Only use nil as a node argument to create the first element upon an empty linked list (list.count is 0).
    ---@param list table
    ---@param node LinkedListNode
    ---@param val any
    function LinkedList.insertBefore(list, node, val)
        local item = {}
        if list.count == 0 then
            item.value = val
            list.first = item
            list.last = item
            item.next = item
            item.prev = item
            list.count = 1
        else
            item.value = val
            item.next = node
            item.prev = node.prev
            node.prev.next = item
            node.prev = item
            list.count = list.count + 1
        end
        --update as the first of the list
        if node == list.first then
            list.first = item
        end
        for i, _ in ipairs(list.onInsertEvents) do
            --print(list.onInsertEvents[i](item)
            list.onInsertEvents[i](item)
        end
        return item
    end
    
    ---Remove a node of the list, return reference to the next node.
    ---@param list LinkedList
    ---@param node LinkedListNode
    ---@return LinkedListNode
    function LinkedList.erase(list, node)
        for i, _ in ipairs(list.onEraseEvents) do
            --print(list.onEraseEvents[i](item)
            list.onEraseEvents[i](node)
        end
        local index = node.next
        node.prev.next = node.next
        node.next.prev = node.prev
        --update the edges of the list
        if node == list.first then
            list.first = node.next
        elseif node == list.last then
            list.last = node.prev
        end
        node = nil
        list.count = list.count - 1
        return index
    end
    ---Clears the linked list, removing all nodes.
    ---@param list LinkedList
    function LinkedList.clear(list)
        list = nil
    end
    ---Iterates through the whole list, avoiding circularity.
    ---Starts from node position, using its value.
    ---code must have at least one parameter, which is each node's value.
    ---@param node table
    ---@param code function
    ---@param ... any
    function LinkedList.forEach(node, code, ...)
        if node then
            local it = node
            repeat
                code(it.value, ...)
                it = it.next
            until it == node
        end
    end
    ---Fires when a node is inserted on the list. Requires to pass one argument on the code function.
    ---Returns the index of the registered event.
    ---@param list LinkedList
    ---@param code fun(insertedNode:LinkedListNode)
    function LinkedList.onInsertAdd(list, code)
        list.onInsertEvents[#list.onInsertEvents + 1] = code
        return #list.onInsertEvents
    end
    ---Fires when a node is erased on the list. Requires to pass one argument on the code function.
    ---Returns the index of the registered event.
    ---@param list LinkedList
    ---@param code fun(erasedNode:LinkedListNode)
    function LinkedList.onEraseAdd(list, code)
        list.onEraseEvents[#list.onEraseEvents + 1] = code
        return #list.onEraseEvents
    end
    ---Removes a function associated with insert events by its index.
    ---Returns the index of the next event.
    ---@param list LinkedList
    ---@param index integer
    function LinkedList.onInsertRemove(list, index)
        table.remove(list.onInsertEvents, index)
        return index
    end
    ---Removes a function associated with insert events by its index.
    ---Returns the index of the next event.
    ---@param list LinkedList
    ---@param index integer
    function LinkedList.onEraseRemove(list, index)
        table.remove(list.onEraseEvents, index)
        return index
    end
end
Some comments: I still don't fully like the way of event functions removal.
I don't know how to make it exactly compatible with a for loop, since this way it would enable the insertion/removal inside the loop. @Eikonium does it on Set/Group Datastructure, for something else.
 
Level 20
Joined
Jul 10, 2009
Messages
478
Nice to see that you've continued working on the resource!

Not sure though, if I understand your comment fully. Do you want to refactor your LinkedList.forEach method into a generic for-loop (which I'd appreciate) or is it about LinkedList.onInsertRemove?
Generic For-loops are a tough topic on their own, but I might write you one for the LinkedList, if you like.

By looking at the hook removals, table.remove would currently shift all indices after the removed one, so the user would lose access to the indices that he had identified with the hooks previously.
You can maybe use the function itself as a parameter to remove it, i.e. LinkedList.onEraseRemove(list, func). That would require double representation of (index,function)-pairs to prevent a complete list search, though (i.e. you'd both need a table t1[index] = function to store the execution order of hooks and a table t2[function] = index to prevent list search upon removal).

Edit: LinkedList.clear looks like it's attempting to destroy the list, but setting the parameter reference to nil won't do anything. If the user wants to destroy the list, he must clear all references to it on his own (and the garbage collector will do the rest). The circular references inside the Linked List get automatically collected by the gc, as discussed, so nilling all nodes one by one is not necessary (and you haven't done so, I just write that for completeness sake :D).
I think I trapped you into keeping the current LinkedList.clear-method with my previous remarks on garbage collection, sorry for that! That one I thought would be refactored into something that empties the list instead of destroying it.

Cheers!
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Hi Wrda, below is the code I've rewritten which tries to bridge my approach with DLL with your approach here. Example API featured below:

Lua:
list = LinkedList.create()

list:insert(function() print "dude 1" end)

list.next:insert(function() print "dude 2" end)

list.next.next:insert(function() print "dude 3" end)

print("list has "..list.n.." dudes")
list:forEach()

list.next:remove()

print("list has "..list.n.." dudes")
list:forEach()


list.next:remove()

print("list has "..list.n.." dudes")
list:forEach()


list:reset()

print("list has "..list.n.." dudes")

Lua:
do
    --Doubly-Linked List v1.0.0.0 by Wrda and Bribe
    
    ---@class LinkedList:table
    ---@field create function
    LinkedList = { __index = function(_, key) return rawget(LinkedList, key) end }
    
    ---@class listHead:LinkedList
    ---@field head listHead
    ---@field next listNode|listHead
    ---@field prev listNode|listHead
    ---@field protected n integer
    ---@field protected reset function
    ---@field protected forEach function
    ---@field insert function
    
    ---@class listNode:listHead
    ---@field remove function
    ---@field func function
    
    ---Creates or resets a table as a LinkedList head.
    ---@param head? table allows a user-specified head or generates a new one.
    ---@return listHead
    function LinkedList.create(head)
        head = head or {} ---@type listHead
        setmetatable(head, LinkedList)
        head.next = head
        head.prev = head
        head.head = head
        head.n = 0
        return head
    end

    ---Node can be an existing table, or a new table will be created to represent the
    ---node. "list" can either be the head, or any point where you want the node to be
    ---inserted. It will insert *after* the given head/node.
    ---
    ---If a func is passed, it will be called with LinkedList.forEach with the node as its
    ---parameter.
    ---@param list listNode|listHead
    ---@param node? listNode
    ---@param func? fun(node:listNode):boolean
    ---@return listNode node that was added to the list (if addition was successful)
    function LinkedList.insert(list, node, func)
        if node then
            if type(node) == "table" then
                if node.head then   --A node cannot be added to more than one list and simply
                    node:remove()   --resetting it will cause errors. Remove it first.
                end
            else
                node, func = {}, node           --User passed an func but not a node.
            end
        end
        local head = list.head
        if head then
            node = node or {}
            setmetatable(node, LinkedList)
            list.next.prev = node
            node.next = list.next
            list.next = node
            node.prev = list
            node.head = head
            head.n = head.n + 1
            node.func = func
            return node
        end
    end

    ---Removes a node from whatever list it is a part of. A node cannot be a part of
    ---more than one list at a time, so there is no need to pass the containing list as
    ---an argument.
    ---@param node listNode
    ---@return boolean wasRemoved
    function LinkedList.remove(node)
        if node then
            local head = node.head
            if head and head ~= node then
                node.prev.next = node.next
                node.next.prev = node.prev
                node.head = nil
                head.n = head.n - 1
                return true
            end
        end
    end

    ---Shows how you can inline this to grant you more flexibility or performance.
    ---@param head listHead
    ---@param func fun(node:listNode):boolean
    ---@return listNode|listHead last_iterated
    local function loop(head, func)
        local node = head.next
        while (node ~= head) do
            if func(node) then break end
            node = node.next
        end
        return node
    end

    ---default loop callback
    ---@param node listNode
    ---@return boolean break_if_true
    local function defaultFunc(node) return node.func and node.func(node) end

    ---Iterates the given list and calls func(node) on each node that is found in the
    ---list. If no "func" is given, it will try to call the dynamic func. If the "func"
    ---returns "true", the loop will break.
    ---@param list listHead
    ---@param func? fun(node:listNode):boolean
    ---@return listNode|listHead last_iterated
    function LinkedList.forEach(list, func)
        if list and list.head then
            return loop(list, func or defaultFunc)
        end
    end

    ---Removes all nodes from the given linked list.
    ---@param list listHead
    ---@return boolean was_destroyed
    function LinkedList.reset(list)
        if list.head and list.head == list then
            list:forEach(function(node) node.head = nil end)
            list.next = list
            list.prev = list
            list.n = 0
            return true
        end
    end
end

Background on why the current resource could be reduced in complexity to what I have written above:


Additionally, the setmetatable and additional tables created at the top of the original LinkedList script don't work correctly. I have adjusted that so that inheritance will now work correctly with the LinkedList and any child nodes. The declarations are made at the top in Emmy Annotation style, thanks to @Eikonium.

@Wrda, feel free to add this version to the Code Submissions forum, or make any desired changes to your code directly and submit them.
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
Not sure though, if I understand your comment fully. Do you want to refactor your LinkedList.forEach method into a generic for-loop (which I'd appreciate) or is it about LinkedList.onInsertRemove?
Generic For-loops are a tough topic on their own, but I might write you one for the LinkedList, if you like.
Yes, refactoring it into a generic for-loop, since it would then be possible to add/remove nodes. If you did this favor for me it would mean the world :D
By looking at the hook removals, table.remove would currently shift all indices after the removed one, so the user would lose access to the indices that he had identified with the hooks previously.
You can maybe use the function itself as a parameter to remove it, i.e. LinkedList.onEraseRemove(list, func). That would require double representation of (index,function)-pairs to prevent a complete list search, though (i.e. you'd both need a table t1[index] = function to store the execution order of hooks and a table t2[function] = index to prevent list search upon removal).
Yes, I was aware of that. I forgot that @Bribe's hook script just makes this a breeze, so in reality I only need 1 function.

Edit: LinkedList.clear looks like it's attempting to destroy the list, but setting the parameter reference to nil won't do anything. If the user wants to destroy the list, he must clear all references to it on his own (and the garbage collector will do the rest). The circular references inside the Linked List get automatically collected by the gc, as discussed, so nilling all nodes one by one is not necessary (and you haven't done so, I just write that for completeness sake :D).
I think I trapped you into keeping the current LinkedList.clear-method with my previous remarks on garbage collection, sorry for that! That one I thought would be refactored into something that empties the list instead of destroying it.

Cheers!

The "destroy" function you are using actually doesn't do anything, since you're only setting a parameter to nil. The closest match in your system to what I'm doing is your "LinkedLIst.erase" method.
Ah I see, so I guess to empty the list one has to loop through all nodes, while destroying it requires to nil all references manually.

Hi Wrda, thanks for your feedback. Your linked list has a lot more features than this one has, and has really good documentation/a smart approach.

1) Your nodes expect a "value" to be passed, that then gets linked to a node, whereas this structure itself delivers nodes directly to the user.
2) Your "insertBefore" and "insertAfter" seem to be overthinking the problem. You only need one of the two, and could reduce the verbosity to just "insert". If you want to "insertBefore" with DLL (as currently it inserts "after" by default when using the "insert" method), then you would insert via the previous node in the list (DLL.insert(nodeAt.prev, node)).
3) Your metatable method of extensibility is very good, and allows members to call method that would otherwise be static. Currently, with DLL, this requires reference to the static methods DLL.remove/DLL.loop rather than the returned tables having those properties themselves. I've been trying to find a way to allow them to be called dynamically, and it looks like you (literally) cracked the code on that one.
4) I don't fully understand the purpose of all the data attachment via args/val that you're using. I think the user can do the data attachment themselves by just associating the data/args themselves (if wished) to the Linked List node as a property of that node.
5) The events you use for the LinkedList can be manually-created via Hook (which, granted, is very new and wasn't around when you were developing this) if the user wished, and don't need to be hardcoded into your script.
6) The first/last members seem to also be a bit bloated compared to just having a "head" node and head.next is "first" and head.prev is "last".

Altogether, yours has a cleaner name, better functionality and fluidity. I will share my proposal on your thread.
I also thank you and Eikonium for being interested in lua stuff with nice ideas. I'm like a big time fan of yours bribe, from all your work on GUI stuff and JASS (also became fan of Eikonium :p)
1) I don't think your way is a deal breaker, it just seemed faster for me to assign a value instantly at insertion.
2) I had "insert" function only before, then I modified it because Eikonium convinced me otherwise. Perhaps it's a matter of preference.
4) Do you mean the node.value? It seemed like a standard thing to do in lua linked lists.
5) Yeah with your hook script it would be easy.
6) Fair enough.
I'm still not so confident in my lua knowledge, so hearing this from you guys gives me motivation.

Hi Wrda, below is the code I've rewritten which tries to bridge my approach with DLL with your approach here. Example API featured below:

Lua:
list = LinkedList.create()

list:insert(function() print "dude 1" end)

list.next:insert(function() print "dude 2" end)

list.next.next:insert(function() print "dude 3" end)

print("list has "..list.n.." dudes")
list:forEach()

list.next:remove()

print("list has "..list.n.." dudes")
list:forEach()


list.next:remove()

print("list has "..list.n.." dudes")
list:forEach()


list:reset()

print("list has "..list.n.." dudes")

Lua:
do
    --Doubly-Linked List v1.0.0.0 by Wrda and Bribe
   
    ---@class LinkedList:table
    ---@field create function
    LinkedList = { __index = function(_, key) return rawget(LinkedList, key) end }
   
    ---@class listHead:LinkedList
    ---@field head listHead
    ---@field next listNode|listHead
    ---@field prev listNode|listHead
    ---@field protected n integer
    ---@field protected reset function
    ---@field protected forEach function
    ---@field insert function
   
    ---@class listNode:listHead
    ---@field remove function
    ---@field func function
   
    ---Creates or resets a table as a LinkedList head.
    ---@param head? table allows a user-specified head or generates a new one.
    ---@return listHead
    function LinkedList.create(head)
        head = head or {} ---@type listHead
        setmetatable(head, LinkedList)
        head.next = head
        head.prev = head
        head.head = head
        head.n = 0
        return head
    end

    ---Node can be an existing table, or a new table will be created to represent the
    ---node. "list" can either be the head, or any point where you want the node to be
    ---inserted. It will insert *after* the given head/node.
    ---
    ---If a func is passed, it will be called with LinkedList.forEach with the node as its
    ---parameter.
    ---@param list listNode|listHead
    ---@param node? listNode
    ---@param func? fun(node:listNode):boolean
    ---@return listNode node that was added to the list (if addition was successful)
    function LinkedList.insert(list, node, func)
        if node then
            if type(node) == "table" then
                if node.head then   --A node cannot be added to more than one list and simply
                    node:remove()   --resetting it will cause errors. Remove it first.
                end
            else
                node, func = {}, node           --User passed an func but not a node.
            end
        end
        local head = list.head
        if head then
            node = node or {}
            setmetatable(node, LinkedList)
            list.next.prev = node
            node.next = list.next
            list.next = node
            node.prev = list
            node.head = head
            head.n = head.n + 1
            node.func = func
            return node
        end
    end

    ---Removes a node from whatever list it is a part of. A node cannot be a part of
    ---more than one list at a time, so there is no need to pass the containing list as
    ---an argument.
    ---@param node listNode
    ---@return boolean wasRemoved
    function LinkedList.remove(node)
        if node then
            local head = node.head
            if head and head ~= node then
                node.prev.next = node.next
                node.next.prev = node.prev
                node.head = nil
                head.n = head.n - 1
                return true
            end
        end
    end

    ---Shows how you can inline this to grant you more flexibility or performance.
    ---@param head listHead
    ---@param func fun(node:listNode):boolean
    ---@return listNode|listHead last_iterated
    local function loop(head, func)
        local node = head.next
        while (node ~= head) do
            if func(node) then break end
            node = node.next
        end
        return node
    end

    ---default loop callback
    ---@param node listNode
    ---@return boolean break_if_true
    local function defaultFunc(node) return node.func and node.func(node) end

    ---Iterates the given list and calls func(node) on each node that is found in the
    ---list. If no "func" is given, it will try to call the dynamic func. If the "func"
    ---returns "true", the loop will break.
    ---@param list listHead
    ---@param func? fun(node:listNode):boolean
    ---@return listNode|listHead last_iterated
    function LinkedList.forEach(list, func)
        if list and list.head then
            return loop(list, func or defaultFunc)
        end
    end

    ---Removes all nodes from the given linked list.
    ---@param list listHead
    ---@return boolean was_destroyed
    function LinkedList.reset(list)
        if list.head and list.head == list then
            list:forEach(function(node) node.head = nil end)
            list.next = list
            list.prev = list
            list.n = 0
            return true
        end
    end
end

Background on why the current resource could be reduced in complexity to what I have written above:


Additionally, the setmetatable and additional tables created at the top of the original LinkedList script don't work correctly. I have adjusted that so that inheritance will now work correctly with the LinkedList and any child nodes. The declarations are made at the top in Emmy Annotation style, thanks to @Eikonium.
Looks good to me.
@Wrda, feel free to add this version to the Code Submissions forum, or make any desired changes to your code directly and submit them.
Ok, will do.
 
Status
Not open for further replies.
Top