• 🏆 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] Lightweight Doubly-Linked List

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
I need a doubly-linked list that creates its own indices for some projects that I'm working on. I've kept the API as simple as possible to allow this resource to serve as a spartan framework for more complex systems.

A system as well-known as this should need little-to-no introduction. The functions and how they work are outlined in the comments below.

One thing I will mention is that you can designate a function that gets called when the list is looped. This will allow functions/anonymous functions to retain their local scope even when looped along with any number of others in the same system.

Lua:
do
--[[--------------------------------------------------------------------------------------
    Lightweight Doubly-Linked List v1.2.0.0 by Bribe
    
    DLL basic API:
        DLL.create() -> Returns a new table representing a Doubly-Linked List.
        DLL.insert(list) -> Adds a new node to the list.
        DLL.loop(list, func) -> calls "func(node)" on all nodes in the list. If "func"
                                returns true, the loop will break. Loop returns the last
                                node that was called.
        DLL.remove(node) -> Removes the node from whichever list it is a part of.
        DLL.destroy(list) -> Destroys the list and removes all of its nodes.
    
    Node API:
        readonly node.next -> the next node in the list.
        readonly node.prev -> the previous node in the list.
        readonly node.head -> the doubly-linked list table.
        node.func -> function called when DLL.loop(list) is invoked without a second
                     parameter.
    
    List API:
        readonly list.size -> the number of nodes in the list.
----------------------------------------------------------------------------------------]]
    
    DLL = {}
    
--[[--------------------------------------------------------------------------------------
    DLL.create
    Args: [head]
    Returns: List node pointing to itself.
    Desc: Can be used to reset an existing list by passing an existing head, or to create
          a list from an existing table. The head is optional and this will create a new
          one if you don't already have one.
----------------------------------------------------------------------------------------]]
    
    function DLL.create(head)
        head = head or {} --allows a user-specified head or generates a new one.
        head.next = head
        head.prev = head
        head.head = head
        head.size = 0
        return head
    end
    
--[[--------------------------------------------------------------------------------------
    DLL.insert
    Args: list[, node, func]
    Returns: Node that was added to the list (if addition was successful)
    Desc: 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 list/node.
          
          If a func is passed, it will be called with DLL.loop with the node as its
          parameter.
----------------------------------------------------------------------------------------]]
    
    function DLL.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
                    DLL.remove(node)   --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 {}
            list.next.prev = node
            node.next = list.next
            list.next = node
            node.prev = list
            node.head = head
            head.size = head.size + 1
            node.func = func
            return node
        end
    end
    
--[[--------------------------------------------------------------------------------------
    DLL.remove
    Args: node
    Desc: 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.
----------------------------------------------------------------------------------------]]
    
    function DLL.remove(node)
        if node then
            local head = node.head
            if head then
                node.prev.next = node.next
                node.next.prev = node.prev
                node.head = nil
                head.size = head.size - 1
                return true
            end
        end
    end
    
--[[--------------------------------------------------------------------------------------
    Internal
    
    Shows how you can inline this to grant you more flexibility or performance.
----------------------------------------------------------------------------------------]]
    
    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
    
    local function defaultFunc(node) return node.func and node.func(node) end
    
--[[--------------------------------------------------------------------------------------
    DLL.loop
    Args: list[, func]
    Desc: 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.
    Rtrn: The last node that is looped is the node that will be returned at the end.
----------------------------------------------------------------------------------------]]
    
    function DLL.loop(list, func)
        if list and list.head then
            return loop(list, func or defaultFunc)
        end
    end
    
--[[--------------------------------------------------------------------------------------
    DLL.destroy
    Args: list
    Desc: Destroys the given linked list.
----------------------------------------------------------------------------------------]]
    
    function DLL.destroy(list)
        if list.head and list.head == list then
            loop(list, function(node) node.head = nil end)
            list.next = nil
            list.prev = nil
            list.head = nil
            return true
        end
    end
end
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
I was going over my vJass resources and realized that I coded this very similar thing 12 years ago: [Snippet] Static List

I find this sort of thing a lot more annoying to program in Lua than it was in vJass, but that could just be preference. Considering how much more powerful this is, and that it doesn’t generate all that extra module code, it is a pretty cool thing.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Updated to version 1.2 - "iterate" has been renamed to "loop". The function passed to loop (or insert) will break that loop if it returns true. DLL.loop will return the last node that was iterated, which can be useful in case the loop breaks early (e.g. if you are inserting based on some kind of priority).
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
I'm not sure, but don't you only to nil the the list in order to destroy the list? You're looping through all nodes and nulling them.
According to this post, you only have to nil the list:
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).
(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.
This was a post on my first attempt to do a circular doubly linked list on lab: Linked List
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
I'm not sure, but don't you only to nil the the list in order to destroy the list? You're looping through all nodes and nulling them.
According to this post, you only have to nil the list:

This was a post on my first attempt to do a circular doubly linked list on lab: Linked List
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.

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.

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.

Edit: Will propose that this resource be graveyarded once Wrda's LinkedList gets published.
 
Last edited:
Top