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

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
This is a merge of [Lua] - Lightweight Doubly-Linked List and [Lua] Linked List, regarding the further polishment of this resource that me, @Bribe and @Eikonium (mostly us) have discussed and dedicated to.
Lastest version:
Lua:
--[[
    Doubly-Linked List v1.5.4.1 by Wrda, Eikonium and Bribe, with special thanks to Jampion
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
    
LinkedList API:
    LinkedList.create() -> LinkedListHead
      - Creates a new LinkedListHead that can have nodes inserted to itself.
    list.head -> the head (internal implementation) of the list. [node.head] has the same result.
    list.next -> next item in the same list. [list.next] is always the first node of the list.
                 Doesn't skip the head.
    list.prev -> previous item in the same list. [list.prev] is always the last node of the list.
                 Doesn't skip the head.
    
    list:insert([value : any, after : boolean]) -> LinkedListNode
      - Inserts *before* the given LinkedList object unless "after" is true.
      - If a value is passed, the system will attach it to the node as a generic "value"
      - Returns the inserted node that was added to the list.
    for node in LinkedList.loop(start : LinkedList [, finish : LinkedList , backwards : boolean]) do [stuff] end
      - Shows how to iterate over all nodes in "list". Inclusive range.
      - An example of how to iterate while skipping the first and last nodes of the list:
        for node in list.next.next:loop(list.prev.prev) do print(node.value) end
        
API specific to LinkedListHead:
    list.remove(node)
      - Removes a node from list.
    list.n
      - The number of LinkedListNodes in the list.
    fromList:merge(intoList : LinkedList [, mergeBefore : boolean])
      - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
        at the beginning, if "mergeBefore" is true).
        "fromList" needs to be the linked list head, but "into" can be anywhere in that list.
      
LinkedListNode API:
    node:remove()
      - Removes a node from whatever list it is a part of.
    node:getNext()
      - Gets the next node, ignoring the head.
    node:getPrev()
      - Gets the previous node, ignoring the head.
    node.value
      - Whatever value might have been assigned from list:insert([value])
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList
---@class LinkedListNode : LinkedList
---@field value any
---@class LinkedListHead : LinkedList
---@field n integer
LinkedList = {}
LinkedList.__index = LinkedList
---Creates a new LinkedList head node.
---@return LinkedListHead new_list_head
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)
    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end
---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode new_node
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)
    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
 
    node.value = value
    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
end
---Removes a node from whatever list it is a part of.
function LinkedList:remove()
    self.prev.next = self.next
    self.next.prev = self.prev
    self.head.n = self.head.n - 1
    self:destroy()
end
---Nullify the linked list's inheritance (as a matter of principle).
function LinkedList:destroy()
    setmetatable(self, nil)
end
--Gets the next node in the sequence of the list it is in, ignoring the head.
function LinkedList:getNext()
    if self.next == self.head then
        return self.head.next
    end
    return self.next
end
--Gets the previous node in the sequence of the list it is in, ignoring the head.
function LinkedList:getPrev()
    if self.prev == self.head then
        return self.head.prev
    end
    return self.prev
end

---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore? boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
 
    for node in from:loop() do node.head = head end
    head.n = head.n + from.n
    from.n = 0
 
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into
    --reset the original list to a simple LinkedListHead
    from.next = from
    from.prev = from
end
---Loop a LinkedList from a starting node/head to a finish node/head in either direction.
---Inclusive range.
---@param start LinkedList
---@param finish? LinkedList
---@param backward? boolean
---@return function
function LinkedList.loop(start, finish, backward)
    local head = start.head
    if head.n == 0 then return end
    local direction = backward and "prev" or "next"
    local skip = start ~= head or start == finish
    if not finish or finish == head then
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
            end
            return start ~= head and start or nil
        end
    else
        return function()
            if start ~= finish or skip then
                if skip and skip ~= 1 then
                    skip = start == finish and 1
                else
                    start = start[direction]
                    if start == head then
                        start = start[direction]
                    end
                end
                return start
            end
        end
    end
end
--End of LinkedList library

Lua:
do
--[[
    Doubly-Linked List v1.0.0.0 by Wrda, Eikonium and Bribe

*****************************************************************************
* A script that allows the possibility to create a sequence of any object   *
* linked together.                                                          *
*****************************************************************************
------------------------------------------------------------------------------
  API:
    LinkedList.create(head) -> listHead
        - Creates or resets a table as a LinkedList head.
        - Allows a user-specified head or generates a new one.
    LinkedList.insert(list, node, func) -> listNode
        - "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 be inserted *after* the given head/node.
        - If a func is passed, it will be called with LinkedList.forEach with the node as its
          parameter.
        - Returns the inserted node that was added to the list (if addition was successful).
    LinkedList.remove(list, node) -> boolean
        - 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.
        - Returns true if it was sucessful.
    LinkedList.forEach(list, func) -> listNode or listHead
        - 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.
    LinkedList.reset(list) -> boolean
        - Removes all nodes from the given linked list.
        - Returns true if sucessful.
  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 loop(list) is invoked without a second
                 parameter.
 
  List API:
    readonly list.size -> the number of nodes in the list.
]]

    ---@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 be inserted *after* the given head/node.
    ---
    ---If a func is passed, it will be called with LinkedList.forEach with the node as its
    ---parameter.
    ---Returns the inserted node that was added to the list (if addition was successful)
    ---@param list listNode|listHead
    ---@param node? listNode
    ---@param func? fun(node:listNode):boolean
    ---@return listNode node
    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.
    ---Returns true if removal was sucessful.
    ---@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 lastIterated
    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 breakIfTrue
    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 lastIterated
    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.
    ---Returns true if succesful.
    ---@param list listHead
    ---@return boolean wasDestroyed
    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
Currently, I've witnessed some issues while testing:
When looped with forEach, it looped retrogradely. I guess it must be an issue with the insert function.
Refactoring forEach with a generic for loop is more ideal, since then it would able to to insert/remove nodes inside it. Also with the possibility to loop starting from a node anywhere in the list.
In local function defaultFunc(node) return node.func and node.func(node) end, won't node.func always return true if node.func() is too? Or better, any value that isn't nil or false returned from node.func() is going to be true. Am I missing something?

Edit: tested with this
Lua:
function test()
    local t = CreateTrigger()
    TriggerRegisterTimerEventSingle(t, 0.20)
    TriggerAddAction(t, function()
            try(function()
                local l = LinkedList.create()
 
                local a = LinkedList.insert(l, {value = "A"})
                local b = LinkedList.insert(l, {value = "B"})
                local c = LinkedList.insert(l, {value = "C"}, function(node) print(node.value) end)
                LinkedList.forEach(l)
                local l2 = LinkedList.create()
                local a2 = LinkedList.insert(l2, {value = "D"})
                local b2 = LinkedList.insert(l2, {value = "E"})
                local c2 = LinkedList.insert(l2, {value = "F"})
                LinkedList.forEach(l2, function(node) print(node.value) end)
            end)
    end)
end
onInit(test)
Prints: C
F
E
D
The C is a normal behavior though.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
The “insert” method always works as “insertAfter”. The idea behind it is to allow addition to the beginning of the list by default. If insertBefore is wished, you could change your demo code to:

Lua:
local l = LinkedList.create()
           
                local a = l.prev:insert({value = "A"})
                local b = l.prev:insert({value = "B"})
                local c = l.prev:insert({value = "C"})
                l:forEach(function(node) print(node.value) end)

local l2 = LinkedList.create()
                local a2 = l2.prev:insert({value = "D"})
                local b2 = l2.prev:insert({value = "E"})
                local c2 = l2.prev:insert({value = "F"})
                l2:forEach(function(node) print(node.value) end)

You can also use Online Lua Compiler to execute simple snippets like this without worrying about firing up WarCraft 3. Just make sure to add the LinkedList library at the top before you do.

I agree that there are benefits to inlining the “forEach” method in certain scenarios, such as the one you described.

won't node.func always return true if node.func() is too? Or better, any value that isn't nil or false returned from node.func() is going to be true.

Yes, if a node.func exists, then call node.func() and if it returns a non-nil/false value then the loop will break. This gives the caller some control over the generic loop.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
This is the generic for-loop for LinkedList:
Lua:
---Enables the generic for-loop for LinkedLists.
---Syntax: for node in LinkedList.loop(list) do print(node.value) end
---Alternative Syntax: for node in list:loop() do print(node.value) end
---@param list listHead
function LinkedList.loop(list)
    local loopNode = list.head
    return function()
        loopNode = loopNode.prev
        if loopNode == list.head then
            return nil
        end
        return loopNode
    end
end

  • The loop is made in a way that it reflects the order of insertion: if b is inserted after a, the loop will go a -> b. That however currently requires it to go in node.prev direction, because as you've already discussed, the insert-method identifies b comes after a with a.prev = b. I intuitively would have expected a.next = b, because for me, next means afterwards. That's probably a matter of taste, though. I leave that to you.
    If you change the interpretation of next and prev, you also need to replace "prev" by "next" in my loop-function.
  • It's also possible to use the __pairs metamethod, which allows you to write for node in pairs(list) do print(node) end instead of the syntax presented above. I chose to not use pairs here, because we obviously don't have (key,value)-pairs, but just single nodes.
    But if you prefer having the pairs-loop, I'm happy to change my code.
  • The generic for-loop currently allows for removing the loop-element from the list, because your remove-method doesn't nil the removed node's next and prev properties. If you choose to nil these in the future, I need to adjust the generic for loop for that in order to still allow for node removal.
  • Generic for-loops also allow for using the break-keyword, which is much more convenient than evaluating return values from loop-callbacks.

Further remarks:
  • I don't currently see the difference between a LinkedList and a listHead. The second inherits from the first, but is LinkedList actually used anywhere? All functions taking a list-parameter seem to expect a listHead. Maybe you can skip on listHead entirely and just use LinkedList?
  • The remove-method doesn't currently allow for object oriented notation. list:remove(node) is not applicable, because remove doesn't take the list as its first argument and node:remove() is not applicable, because nodes don't currently have a metatable pointing to the class. I'd say, list:remove(node) is more in line with the other syntax chosen for this resource, so I'd suggest adding the list as first argument to remove.
  • I've been confused about why the insert method takes a function as third argument. The description says that it allows to loop over the list after insertion, but I don't see the function being executed in the code (except the line node.func = func, which I'm also confused about) and I wonder, why we need it at all. Isn't the looping-functionality already covered by the forEach and loop methods?
    It looks like the func-parameter has been designed to replace the deprecated onInsert-hook, but I'm not sure. If so, it certainly adds confusing overhead, because doing something only on the inserted node would require using a function that returns true by definition of forEach (assuming you wanted to use forEach during insert). I say confusing, because these details make it very hard to understand your own code after coming back to it after a break. I'd say, the generic for-loop solves the loop breaking-stuff more convienently and you could just deprecate the func parameter from the insert-method. :)
  • Instead of the func-parameter, you might think of adding an optional insertBefore_yn-parameter that you can set to true to insert the new node before instead of afterwards. That is something I personally see use cases for, especially when you want your List to be sorted in any way. Following the conversation in this thread, I can see that @Bribe has a different taste on this, but maybe an optional parameter is a good compromise at this point.
  • The LinkedList currently requires users to create List Nodes as tables before inserting them, but also kind of allows non-table values for the node parameter? I'm confused about this. If I pass anything else than a table, then node is replaced by an empty table and the original value doesn't seem to be used at all?
    If the insert-method was intended to build a new node around the non-table value, then you would have to replace node = {} with node = {value = node}, but I'm not a fan of this either, because it brings new design problems. First, requiring users to create their own nodes doesn't guarantee that they choose the value-property to store their value. Second, this node-pack for non-nodes thing doesn't really work, if the user wants to pass an object from any class, because objects are always tables already, which might already use properties like next, head, etc.
    Also, requiring the user to pass a node to the insert-method brings up the additional problem of that the node might already be contained in another list, which you solve by removing the node first. Removing is maybe not required at all, because inserting a node from another list doesn't seem sensible to me anyway - if at all, the user wants the same value in another list, not the same node (but maybe I'm thinking too short-sighted :D).
  • From my perspective, you could just decide on what to support and what not and provide a clear design vision for the user. I prefer all methods to be easy to use and understand, so I tend to avoid flexibility that comes at the cost of unnecessary complexity. Heavy overloading is one example for that type of complexity that requires users to think twice upon using any method of the resource.

Overall, thanks to both of you for your dedication. I'm looking forward to see the most elaborate LinkedList in the internet, hehe :D
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
@Wrda @Eikonium, the below script should be the "best of all worlds". Thanks very much for showing us how to use custom Lua "for" loops!

Lua:
do
--[[
    Doubly-Linked List v1.1.0.0 by Wrda, Eikonium and Bribe
*****************************************************************************
* A script that allows the possibility to create a sequence of any object   *
* linked together.                                                          *
*****************************************************************************
------------------------------------------------------------------------------
  API:
    LinkedList.create(head) -> LinkedList
        - Creates or resets a table as a LinkedList head.
        - Allows a user-specified head or generates a new one.
        list:insert([node_or_value, before]) -> listNode
        - Inserts *after* the given list/node unless "before" is true.
        - If a "node or value" is passed, the system will check if the node is
          a table. If the table isn't yet part of a LinkedList structure, it
          will be converted into the node that is returned at the end of this
          function. If the table is a part of the LinkedList structure, or a
          different type of value than a table, then it will be assigned as a
          generic "value" (mapped to node.value)
        - Returns the inserted node that was added to the list (if addition was successful).
        list:remove(node) -> boolean
        - Removes a node from whatever list it is a part of.
        - Returns true if it was sucessful.
    for node in list:loop([before]) do [stuff] end
        - Iterates over all nodes in "list".
        list.reset() -> boolean
        - Removes all nodes from the given linked list.
        - Returns true if sucessful.
    List API:
        readonly list.n -> the number of nodes in the list.
]]
    ---@class LinkedList:table
    ---@field head LinkedList
    ---@field next listNode|LinkedList
    ---@field prev listNode|LinkedList
    ---@field n integer
    LinkedList = { __index = function(_, key) return rawget(LinkedList, key) end }
    
    ---@class listNode:LinkedList
    ---@field remove fun(node)->boolean
    
    ---Creates or resets a table as a LinkedList head.
    ---@param head? LinkedList allows a user-specified head or generates a new one.
    ---@return LinkedList
    function LinkedList.create(head)
        if head and type(head) == "table" then
            if head.head and head.n > 0 then
                head:reset()
            end
        else
            head = {}
        end
        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.
    ---@param list listNode|LinkedList
    ---@param node_or_value? any
    ---@param before_yn? boolean
    ---@return listNode node that was added to the list (if addition was successful)
    function LinkedList.insert(list, node_or_value, before_yn)
        local node, value
        if node_or_value then
            if type(node_or_value) == "table" then
                if node_or_value.head then --table is already part of a linked list. Treat it as a "value"
                    value = node_or_value
                else
                    node = node_or_value --table will be transmuted into the linked list node itself.
                end
            else
                --User passed a non-table value.
                value = node_or_value
            end
        end
        if before_yn then list = list.prev end
        local head = list.head
        if head then
            node = node or {}   ---@type listNode
            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.value = value
            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)
        node = node or self
        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
    ---Enables the generic for-loop for LinkedLists.
    ---Syntax: "for node in LinkedList.loop(list) do print(node) end"
    ---Alternative Syntax: "for node in list:loop() do print(node) end"
    ---@param list LinkedList
    ---@param backward? boolean
    function LinkedList.loop(list, backward)
        list = list.head
        local loopNode = list
        local direction = backward and "prev" or "next"
        return function()
            loopNode = loopNode[direction]
            return loopNode ~= list and loopNode or nil
        end
    end
    ---Removes all nodes from the given linked list.
    ---@param list LinkedList
    ---@return boolean was_destroyed
    function LinkedList.reset(list)
        if list.head and list.head == list then
            for node in list:loop() do node.head = nil end
            list.next = list
            list.prev = list
            list.n = 0
            return true
        end
    end
end

Demo script for test purposes. Can be run on Online Lua Compiler if you want to try it out.

Lua:
l = LinkedList.create()

l:insert("a")
l:insert("b")
l:insert("c")
l:insert("d")

for node in l:loop(true) do print(node.value) end

l.next:remove()

for node in l:loop() do print(node.value) end

l.prev:remove()
for node in l:loop(true) do print(node.value) end

l:reset()

for node in l:loop(true) do print(node.value) end

l:insert("new A")
l:insert("new B")
l:insert("new C")
l:insert("new D")


for node in l:loop(true) do print(node.value) end
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
Seems good to me!
The only thing I have to wrap my head around sometimes is to insert at the end of the list :) but I can adapt to that.
It seems to be missing a : on the documentation of list.reset() -> boolean, right at the top.
Also, I would make all functions all the API on the same level as indention, while the comments after each function have a different one, just to make it easier to read/see.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Seems good to me!
The only thing I have to wrap my head around sometimes is to insert at the end of the list :) but I can adapt to that.
It seems to be missing a : on the documentation of list.reset() -> boolean, right at the top.
Also, I would make all functions all the API on the same level as indention, while the comments after each function have a different one, just to make it easier to read/see.
Inserting "before" should be the default, I agree. Ok, I've made the changes now, and:

1) added a new "merge" method.
2) added an "onRemove" function to listNode. By default, it just sets node.head to "nil", but now allows itself to be hooked in whatever way the user wants it to be (either via Hook or via normal overriding).

Lua:
do
    --[[
        Doubly-Linked List v1.3.0.0 by Wrda, Eikonium and Bribe
        ------------------------------------------------------------------------------
        A script that allows the possibility to create a sequence of any object
        linked together.
        ------------------------------------------------------------------------------
    API:
        LinkedList.create(head) -> LinkedList
        - Creates or resets a table as a LinkedList head.
        - Allows a user-specified head or generates a new one.
    
        list:insert([node_or_value, after]) -> listNode
        - Inserts *before* the given list/node unless "after" is true.
        - If a "node or value" is passed, the system will check if the node is
          a table. If the table isn't yet part of a LinkedList structure, it
          will be converted into the node that is returned at the end of this
          function. If the table is a part of the LinkedList structure, or a
          different type of value than a table, then it will be assigned as a
          generic "value" (mapped to node.value)
        - Returns the inserted node that was added to the list (if addition was successful).
    
        list:remove(node) -> boolean
        - Removes a node from whatever list it is a part of and calls self:onRemove().
        - Returns true if it was sucessful.
    
        for node in list:loop([backwards]) do [stuff] end
        - Iterates over all nodes in "list".
    
        fromList:merge(intoList[, backwardsFrom, backwardsInto])
        - Removes all nodes from one list and adds them into another.
    
        list:reset() -> boolean
        - Removes all nodes from the given linked list, calling node:onRemove() on each.
        - Returns true if sucessful.
    
        - readonly list.n : integer -> the number of nodes in the list.
    ]]
        ---@class LinkedList:table
        ---@field head LinkedList
        ---@field next listNode|LinkedList
        ---@field prev listNode|LinkedList
        ---@field n integer
        LinkedList = {}
        LinkedList.__index = LinkedList
    
        ---@class listNode:LinkedList
        ---@field remove fun(node:listNode)->boolean
        ---@field onRemove fun(self:listNode)
    
        ---Creates or resets a table as a LinkedList head.
        ---@param head? LinkedList allows a user-specified head or generates a new one.
        ---@return LinkedList
        function LinkedList.create(head)
            if head and type(head) == "table" then
                local h = head.head
                if h then
                    if h ~= head then
                        return --user passed an active ListNode. This is not allowed.
                    end
                    if h.n > 0 then --first empty any lists that still have nodes.
                        for node in head:loop() do node:onRemove() end
                    end
                end
            else
                head = {}
            end
            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 *before* the given head/node, unless "backward" is true.
        ---@param list listNode|LinkedList
        ---@param node_or_value? any
        ---@param insertAfter? boolean
        ---@return listNode node that was added to the list (if addition was successful)
        function LinkedList.insert(list, node_or_value, insertAfter)
            if not list then return end
            local head = list.head
            if not head then return end
            local node, value
    
            if node_or_value then
                if type(node_or_value) == "table" then
                    if node_or_value.head then --table is already part of a linked list. Treat it as a "value"
                        value = node_or_value
                    else
                        node = node_or_value --table will be transmuted into the linked list node itself.
                    end
                else
                    --User passed a non-table value.
                    value = node_or_value
                end
            end
            if insertAfter then list = list.next end
    
            node = node or {}   ---@type listNode
            setmetatable(node, LinkedList)
            list.prev.next = node
            node.prev = list.prev
            list.prev = node
            node.next = list
            node.head = head
            head.n = head.n + 1
            node.value = value
            node.onRemove = function() node.head = nil end
            return node
        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)
            node = node or self
            if node then
                local head = node.head
                if head and head ~= node then
                    node.prev.next = node.next
                    node.next.prev = node.prev
                    head.n = head.n - 1
                    node:onRemove()
                    return true
                end
            end
        end
    
        ---Enables the generic for-loop for LinkedLists.
        ---Syntax: "for node in LinkedList.loop(list) do print(node) end"
        ---Alternative Syntax: "for node in list:loop() do print(node) end"
        ---@param list LinkedList
        ---@param backward? boolean
        function LinkedList.loop(list, backward)
            list = list.head
            local loopNode = list   ---@type listNode
            local direction = backward and "prev" or "next"
            return function()
                loopNode = loopNode[direction]
                return loopNode ~= list and loopNode or nil
            end
        end
    
        ---Merges LinkedList "from" to another LinkedList "into"
        ---@param from LinkedList|listNode
        ---@param into LinkedList|listNode
        ---@param backwardFrom boolean
        ---@param backwardInto boolean
        function LinkedList.merge(from, into, backwardFrom, backwardInto)
            if from and from.head and into and into.head then
                local directionFrom = backwardFrom and "prev" or "next"
                local n, v
                local node = from[directionFrom]
                from = from.head
                while node ~= from do
                    n, v = node[directionFrom], node.value
                    node:remove()
                    into:insert(node, backwardInto).value = v
                    node = n
                end
            end
        end
    
        ---Removes all nodes from the given linked list.
        ---@param list LinkedList
        ---@return boolean was_reset
        function LinkedList.reset(list)
            return list:create() ~= nil
        end
    end
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
@Wrda, if you are satisfied with version 1.3, then please go ahead and update your original post to include that version, instead (along with any other changes you might want to see).

Version 1.3 is featured in Lua Damage Engine 2.0 for almost all key resources, so I can attest that it is working correctly in a busy environment.
 
Level 20
Joined
Jul 10, 2009
Messages
477
That was some nice improvement with the last version!

One issue I still want to adress is that the insert-method uses a type(node_or_value) == "table"-check to decide on whether to interpret it as a node or a value. That check implies I currently cannot insert a table as a value, but for object-oriented programmers, wanting to insert an object (= table) is a common thing. Given an object A that we want to insert, LinkedList.insert(existingNode, A) would currently manipulate properties of A, which can be unexpected behaviour, especially if the user expects that he just inserted a value that would automatically wrapped into a node.

From my perspective, we can just change the insert-method to take values only (but please let me know, if you had a clever use case for node insertion).
Clearly, taking values requires creating a new node around the value on every insert, but it also simplifies usage and gets rid of unexpected behaviour.
I'd even say, inserting an existing node into a list would never make sense anyway, because by design a node can't be contained in two different LinkedLists, so inserting values should be all we ever want?

That new insert-method can look like this:
Lua:
---Packs the specified value into a new node and inserts it before the specified node.
---Setting insertAfter to true will insert the value after the given node instead.
---@param value any
---@param insertAfter? boolean default: false
---@return listNode node that was added to the list (if addition was successful)
function LinkedList:insert(value, insertAfter)
    local head = self.head
    if insertAfter then self = self.next end
    ---@type listNode --change this to LinkedList, when the listNode type is deprecated
    local newNode = {
        value = value,
        head = head,
        prev = self.prev,
        next = self
    }
    newNode.onRemove = function() newNode.head = nil end --not sure, why we need this
    setmetatable(newNode, LinkedList)
    self.prev.next = newNode
    self.prev = newNode
    head.n = head.n + 1
    return newNode
end

I changed to :-notation, as the method is defined on the LinkedList-class and every list node is also a LinkedList by how you defined the inheritance. I.e. the insert-method can be used on the head or any existing node with node:insert(value).
Now where I see the current state of development, maybe we don't even need the listNode class and can just let all nodes have type LinkedList.
I also removed input param validation, because when the user inputs wrong parameters, I prefer the function throwing errors over just doing nothing.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I like the idea of merging listNode into LinkedList, but I am wary of properties like “n” and “reset/create” being thought of as properties of the list’s nodes when they are definitely not.

One thing I’d like to bring to your attention as to why LinkedList can be transmuted from an existing table: pre-building a LinkedList (such as in the example below).

Lua:
        local id                        = {} ---@type damageEventRegistry
        lastRegistered                  = id
        
        id.filters                      = {}
       
        id.levelsDeep                   = 0
        id.trig                         = trig
        lbs                             = lbs or 1.00
        id.weight                       = lbs
        id.func                         = func
        
        local insertAt = head
        for node in head:loop() do if node.weight > lbs then insertAt = node; break end end
        insertAt:insert(id)
        
        --print("Registered new event to " .. var)
        return lastRegistered

Now in the above case, I could have processed the list iteration first and then done everything afterwards by building off of that node. There could be a future in disallowing nodes from being able to be transmuted from tables, and you might be onto something, but I’m not sure I am quite content with it (yet?)

If someone has been working with, and developing their own table over a long period of time, and they don’t want it to get converted into a listNode, they would just pass nil as the insert value and then separately attach their table as an arbitrary property of the listNode.

Edit: found another reason why pre-building nodes is useful: in [Lua] - Timed Call and Echo in the Echo method, some properties (like .elapsed) are pre-assigned, but it isn’t clear right away which list it will go into.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
I like the idea of merging listNode into LinkedList, but I am wary of properties like “n” and “reset/create” being thought of as properties of the list’s nodes when they are definitely not.
Your current class definition already says that n is a node property. You made it a property of LinkedList and listNode inherits LinkedList (taking over all properties, including n).
But I totally understand that you want to distinguish between the structure holding n (i.e. the head) and other nodes.
Maybe the following class definition would achieve what we both expect:
Lua:
---@class LinkedList : table
---@field next LinkedList
---@field prev LinkedList
---@field value any
---@field head LinkedListHead
---@field onremove function

---@class LinkedListHead : LinkedList
---@field n integer

One thing I’d like to bring to your attention as to why LinkedList can be transmuted from an existing table: pre-building a LinkedList

Prebuilding your stuff is perfectly possible with the method I've proposed. It's just that the desired properties now stay one level deeper in the node. Your two examples would of course need to be adapted to the new insert-method.

Lua:
--Prebuilding some data structure
data1 = {a = 1, b = 2}
data2 = {a = 5, b = 10}
--Inserting it into a LinkedList
l = LinkedList.create()
l:insert(data1)
l:insert(data2)
--Processing the data
for node in l:loop() do
    print(node.value.a + node.value.b) --> prints "3" and "15"
end

One could even think of an additional loop-method that just loops over the values, but I think having to write node.value inside the loop is not too much of a drawback.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Ok, I can see that you are going for the "simple" "no training wheels" approach with the structure. Probably for the best. I have renamed the sub-classes to LinkedListNode and LinkedListHead to indicate that "n" is a head-property and "value" is a node-property.

The below trims down the API quite a bit. I suppose if the user wants an "onDestroy", "merge" or "flush/reset", then they can extend off of this resource rather than bloating this resource with such things.

If one wants to reduce table quantity, then they can still attach data to the LinkedListNode after it is created, rather than pre-building a table and then sending it into the LinkedList construct.

Lua:
--[[
    Doubly-Linked List v1.4.0.0 by Wrda, Eikonium and Bribe
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedList head that can have nodes inserted to itself.

    list:insert([value, after]) -> LinkedListNode
    - Inserts *before* the given head/node unless "after" is true.
    - If a value is passed, the system will attach it as a generic "value"
    - Returns the inserted node that was added to the list (if addition was successful).

    list:remove(node : LinkedListNode)
    - Removes a node from whatever list it is a part of.

    for node in list:loop([backwards]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    
    - readonly list.n : integer -> the number of nodes in the list.
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList

---@class LinkedListNode : LinkedList
---@field value any

---@class LinkedListHead : LinkedList
---@field n integer

LinkedList = {}
LinkedList.__index = LinkedList

---Creates a new LinkedList head node.
---@return LinkedListHead
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)
    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end

---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode node that was added to the list (if addition was successful)
function LinkedList:insert(value, insertAfter)
    local head = self.head

    if insertAfter then self = self.next end
    
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)
    self.prev.next = node
    node.prev = self.prev
    self.prev = node
    node.next = self
    node.head = head
    head.n = head.n + 1
    node.value = value
    return node
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 LinkedListNode
---@return boolean wasRemoved
function LinkedList:remove(node)
    node.prev.next = node.next
    node.next.prev = node.prev
    self.n = self.n - 1
end

---Enables the generic for-loop for LinkedLists.
---Syntax: "for node in LinkedList.loop(list) do print(node) end"
---Alternative Syntax: "for node in list:loop() do print(node) end"
---@param list LinkedList
---@param backward? boolean
function LinkedList.loop(list, backward)
    list = list.head        ---@type LinkedListHead
    local loopNode = list   ---@type LinkedListNode
    local direction = backward and "prev" or "next"
    return function()
        loopNode = loopNode[direction]
        return loopNode ~= list and loopNode or nil
    end
end
 
Level 20
Joined
Jul 10, 2009
Messages
477
That looks good!
I like how the LinkedList discussion went over two threads of elaborate and long discussions to the current state of simplicity. 😂

Just for clarity, I didn't dislike the additional methods at all. My own resources are often blown up with methods, because I tend to think that they "don't hurt". The only thing I try to always push for is clarity of use and prevention of unexpected behaviour. For that reason, feel free to add what comes to your mind. merge, removeValue, pop, clear etc. are all fine.

Ofc the current state is also perfectly fine and should be sufficient for most use cases.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
That looks good!
I like how the LinkedList discussion went over two threads of elaborate and long discussions to the current state of simplicity. 😂

Just for clarity, I didn't dislike the additional methods at all. My own resources are often blown up with methods, because I tend to think that they "don't hurt". The only thing I try to always push for is clarity of use and prevention of unexpected behaviour. For that reason, feel free to add what comes to your mind. merge, removeValue, pop, clear etc. are all fine.

Ofc the current state is also perfectly fine and should be sufficient for most use cases.

Well my original wish was for a “spartan framework” for a Linked List. I think your comments just helped me decide what to chip away at in order to achieve it. If anything beyond that is allowed, why not add everything? So yeah, I don’t want it to be a mess, either.

Version 1.4 is still way better than what I had originally come up with, with much clearer purpose and syntax, thanks to Wrda/your feedback on how how to use the metatable correctly, and also to you for the really clean “loop” method.

If someone wants a protected “remove/destroy” it’s easy enough to extend a class off of LinkedList a little bit and wrap those methods in a protected shell.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Lols, It did look good for me with the merge method, but it's fine with this too.
Simplicity is good :D
You know, the "merge" method is extremely painful to write outside of LinkedList. I was working on an update to "Timed" and it made me realize that the user would lose access to the original node once something from the queue is merged into the primary list.

The below code re-adds an even smoother "merge" method while also keeping the other functions/syntax as simple as before.

Lua:
--[[
    Doubly-Linked List v1.4.1.0 by Wrda, Eikonium and Bribe
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedList head that can have nodes inserted to itself.

    list:insert([value, after]) -> LinkedListNode
    - Inserts *before* the given head/node unless "after" is true.
    - If a value is passed, the system will attach it as a generic "value"
    - Returns the inserted node that was added to the list (if addition was successful).

    list:remove(node : LinkedListNode)
    - Removes a node from whatever list it is a part of.

    for node in list:loop([backwards]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    
    fromList:merge(intoList[, backwardsFrom, backwardsInto])
    - Removes all nodes from one list and adds them into another.
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList

---@class LinkedListNode : LinkedList
---@field value any

---@class LinkedListHead : LinkedList
---@field n integer

LinkedList = {}
LinkedList.__index = LinkedList

---Creates a new LinkedList head node.
---@return LinkedListHead
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)
    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end

local function add(from, node)
    local head = from.head
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
    node.head = head
    head.n = head.n + 1
end

---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode node that was added to the list (if addition was successful)
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)
    add(insertAfter and self.next or self, node)
    node.value = value
    return node
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 LinkedListNode
---@return boolean wasRemoved
function LinkedList:remove(node)
    node.prev.next = node.next
    node.next.prev = node.prev
    self.n = self.n - 1
end

---Merges LinkedList "from" to another LinkedList "into"
---@param from LinkedList
---@param into LinkedList
---@param backwardFrom? boolean
---@param backwardInto? boolean
function LinkedList.merge(from, into, backwardFrom, backwardInto)
    local directionFrom = backwardFrom and "prev" or "next"
    local n, i = nil, 0
    local node = from[directionFrom]
    into = backwardInto and into.next or into
    from = from.head
    while node ~= from do
        i = i + 1
        n = node[directionFrom]
        add(into, node)
        node = n
    end
    from.n = from.n - i
end

---Enables the generic for-loop for LinkedLists.
---Syntax: "for node in LinkedList.loop(list) do print(node) end"
---Alternative Syntax: "for node in list:loop() do print(node) end"
---@param list LinkedList
---@param backward? boolean
function LinkedList.loop(list, backward)
    list = list.head        ---@type LinkedListHead
    local loopNode = list   ---@type LinkedListNode
    local direction = backward and "prev" or "next"
    return function()
        loopNode = loopNode[direction]
        return loopNode ~= list and loopNode or nil
    end
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Hmm, why does merge insert all nodes one by one instead of just reconnecting the first and the last node of from?
Ofc reversing the order would not possible that way, but I find reducing runtime from O(n) to O(1) much more important.

That’s actually a really great idea as usual! I will still have to loop over each node to reassign the head, but it won’t have all that extra overhead from re-linking the in-between nodes.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
This integrates that even faster merge-method proposed by Eikonium:

Lua:
--[[
    Doubly-Linked List v1.4.2.1 by Wrda, Eikonium and Bribe
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedList head that can have nodes inserted to itself.

    list:insert([value : any, after : boolean]) -> LinkedListNode
    - Inserts *before* the given head/node unless "after" is true.
    - If a value is passed, the system will attach it as a generic "value"
    - Returns the inserted node that was added to the list (if addition was successful).

    list:remove(node : LinkedListNode)
    - Removes a node from whatever list it is a part of.

    for node in list:loop([backwards : boolean]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    
    fromList:merge(intoList : LinkedList[, mergeBefore : boolean])
    - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
      at the beginning, if "mergeBefore" is true).
      "fromList" needs to be the linked list head, but "into" can be anywhere in that list.
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList

---@class LinkedListNode : LinkedList
---@field value any

---@class LinkedListHead : LinkedList
---@field n integer

LinkedList = {}
LinkedList.__index = LinkedList

---Creates a new LinkedList head node.
---@return LinkedListHead
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)

    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end

---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode node that was added to the list (if addition was successful)
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)

    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
    
    node.value = value

    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
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 LinkedListNode
---@return boolean wasRemoved
function LinkedList:remove(node)
    node.prev.next = node.next
    node.next.prev = node.prev
    self.n = self.n - 1
end

---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
    from.n = 0
    for node in from:loop() do node.head = head end
    
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into
end

---Enables the generic for-loop for LinkedLists.
---Syntax: "for node in LinkedList.loop(list) do print(node) end"
---Alternative Syntax: "for node in list:loop() do print(node) end"
---@param list LinkedList
---@param backward? boolean
function LinkedList.loop(list, backward)
    list = list.head        ---@type LinkedListHead
    local loopNode = list   ---@type LinkedListNode
    local direction = backward and "prev" or "next"
    return function()
        loopNode = loopNode[direction]
        return loopNode ~= list and loopNode or nil
    end
end --End of LinkedList library
(Edited as previous version 1.4.2 only took into account the nodes pointing forwards, which won't work at all).
 
Last edited:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
The new merge has some problems.
The size of the merged/into list is not updated.
The size of the cleared/from list is not updated and it still contains nodes. Looping over it results in an infinite loop.
I think what happens is that the next and prev now point to the into list, which is traversed indefinitely, because it never reaches the head of the from list.

The remove function says:
"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."
But the containing list is given as implicit self parameter. It also accesses the n field of the self parameter, so it would only work if used on LinkedListHead. There is no return value, so it looks like either documentation or the code need to be fixed.
The insert function also has a discrepancy between documentation and code. The documentation mentions "backward", which doesn't exist.

Any reason why insert and remove use : syntax, but merge and loop don't?
Why does loop always start at the head?

I was a bit confused by the class names. If I understood them correctly they work like this:
LinkedListHead: Dummy node that works as unique identifier for the list
LinkedListNode: Value node
LinkedList: Abstract parent class of the two classes above
What confused me is that LinkedListHead is used to refer to the list rather than LinkedList and LinkedList.create() returns a LinkedListHead.
Maybe just some more explanations in the documentation would be helpful.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
The new merge has some problems.
The size of the merged/into list is not updated.
The size of the cleared/from list is not updated and it still contains nodes. Looping over it results in an infinite loop.
I think what happens is that the next and prev now point to the into list, which is traversed indefinitely, because it never reaches the head of the from list.

The remove function says:
"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."
But the containing list is given as implicit self parameter. It also accesses the n field of the self parameter, so it would only work if used on LinkedListHead. There is no return value, so it looks like either documentation or the code need to be fixed.
The insert function also has a discrepancy between documentation and code. The documentation mentions "backward", which doesn't exist.

Any reason why insert and remove use : syntax, but merge and loop don't?
Why does loop always start at the head?

I was a bit confused by the class names. If I understood them correctly they work like this:
LinkedListHead: Dummy node that works as unique identifier for the list
LinkedListNode: Value node
LinkedList: Abstract parent class of the two classes above
What confused me is that LinkedListHead is used to refer to the list rather than LinkedList and LinkedList.create() returns a LinkedListHead.
Maybe just some more explanations in the documentation would be helpful.

Really great input - thanks!

The merge method is now fixed for all reported issues and moves the node count accordingly.

The reason why "merge" and "loop" don't use ":" syntax is because it would require an extra local variable declaration.
(loop) I don't think that the "self" inferred parameter can be cascaded downwards to looped embedded functions.
(merge) I prefer the name "from" rather than "self" for clarity.

The "loop" method now allows an optional "finish" parameter to be assigned. If it is given, then the list will traverse slightly slower while checking to make sure to skip the head node. In either case, the list will iterate from whatever the first argument is that you gave it, rather than being limited to starting from the head node. I think there is no performance penalty in implementing the changes you wanted, so I believe this is a win/win.

The documentation has been heavily updated to include as much description as I can think of in terms of breaking down the classes and how each are represented.

Instead of filling up the forum posts with more copy&paste, I've updated the "hidden" script in Wrda's original post (that way Wrda doesn't have to go in and do it manually).

Once this is considered "stable" (as in, not being updated every 3 seconds), I think it can be approved.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
The remove function says:
"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."
But the containing list is given as implicit self parameter. It also accesses the n field of the self parameter, so it would only work if used on LinkedListHead. There is no return value, so it looks like either documentation or the code need to be fixed.
The insert function also has a discrepancy between documentation and code. The documentation mentions "backward", which doesn't exist.

Any reason why insert and remove use : syntax, but merge and loop don't?
Why does loop always start at the head?
I think there might have been a confusion about the : suger syntax does. LinkedList.remove(self, node) is exactly the same as LinkedList:remove(node), it just allows the cut of the first parameter and makes it usable before the :. Essencially all methods (except create) could have been :.

Was about to tackle some of the points, but speedy gonzales beat me to it :D
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Was about to tackle some of the points, but speedy gonzales beat me to it :D
You're referring to the guy who wrote the best, most efficient script of all time:

 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
You're referring to the guy who wrote the best, most efficient script of all time:

Too bad I almost everything to lua, now it's time to make one in lua, perhaps then speed can be increased by 10000%
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
The loop produces unexpected results.

Lua:
function LinkedList:print()
    if self.head.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
    end
    s = "["
    for node in self:loop(self.prev) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end

myList = LinkedList.create()
a = myList:insert(1)
b = myList:insert(2)
c = myList:insert(3)
myList:print()
a:print()
b:print()
c:print()

I have a list with 3 elements and I want to print all elements. The third is printed outside the loop, so I only have "," between the values.
The output is:
Code:
[1, 2, 3]
[2, 3, 3]
[3, 1, 1]
[1, 1, 2]
For the whole list it's correct, but when starting at a specific node, it actually starts at the next node. Is this intentional? It seems weird in this context, but when you want to use myList.next:loop() to skip the first element, it makes sense.
Additionally, the head is not skipped properly. The last print call starts at the next node, which is the head. This is skipped and returns node 1 instead. However, the skipping only affects what is returned, but does not affect the next iteration. So the next iteration is node 1 again.

I'd expect the output to be:
Code:
[1, 2, 3]
[1, 2, 3]
[2, 3, 1]
[3, 1, 2]
So that it follows common loop pattern, where the start is part of the iteration, but the end is not.

Using list.loop(list.prev) to skip the last element does not always work. if list.prev is the head it would be skipped anyway and you end up no skipping anything. Is this something the user has to fix by checking if list.prev is the head and then calling list.loop(list.prev.prev) instead?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Thank you for the bug report. It took me a long time to narrow it down, but it involves a two-factor change to the code. The first, update the LinkedList to the below (I'll let @Wrda update their post this time)

Lua:
--[[
    Doubly-Linked List v1.5.1.0 by Wrda, Eikonium and Bribe, with special thanks to Jampion
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
LinkedList API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedListHead that can have nodes inserted to itself.

    list.next -> next item in the same list
    list.prev -> previous item in the same list

    list:insert([value : any, after : boolean]) -> LinkedListNode
    - Inserts *before* the given LinkedList object unless "after" is true.
    - If a value is passed, the system will attach it to the node as a generic "value"
    - Returns the inserted node that was added to the list.

    for node in LinkedList.loop(start : LinkedList[, finish : LinkedList , backwards : boolean]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    - An example of how to iterate while skipping the first and last nodes of the list:
        for node in list.next:loop(list.prev) do print(node.value) end

API specific to LinkedListHead:
    list.remove(node)
    - Removes a node from list.

    list.n
    - The number of LinkedListNodes in the list.

    fromList:merge(intoList : LinkedList[, mergeBefore : boolean])
    - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
      at the beginning, if "mergeBefore" is true).
      "fromList" needs to be the linked list head, but "into" can be anywhere in that list.

LinkedListNode API:
    node:remove()
    - Removes a node from whatever list it is a part of.

    node.value
    - Whatever value might have been assigned from list:insert([value])
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList

---@class LinkedListNode : LinkedList
---@field value any

---@class LinkedListHead : LinkedList
---@field n integer

LinkedList = {}
LinkedList.__index = LinkedList

---Creates a new LinkedList head node.
---@return LinkedListHead new_list_head
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)

    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end

---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode new_node
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)

    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
    
    node.value = value

    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
end

---Removes a node from whatever list it is a part of.
function LinkedList:remove()
    self.prev.next = self.next
    self.next.prev = self.prev
    self.head.n = self.head.n - 1
end

---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore? boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
    
    for node in from:loop() do node.head = head end
    head.n = head.n + from.n
    from.n = 0
    
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into

    --reset the original list to a simple LinkedListHead
    from.next = from
    from.prev = from
end

---Loop a LinkedList from a starting node/head to a finish node/head in either direction.
---@param start LinkedList
---@param finish? LinkedList
---@param backward? boolean
---@return function
function LinkedList.loop(start, finish, backward)
    local head = start.head
    if head.n == 0 then return end
    local direction = backward and "prev" or "next"
    local skip = start ~= head
    if not finish or finish == head then
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
            end
            return start ~= head and start or nil
        end
    else
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
                if start == finish then return end
                if start == head then
                    start = start[direction]
                    if start == finish then return end
                end
                return start
            end
            return start
        end
    end
end
--End of LinkedList library

The second is a change to your print method. When you loop, you need to set the end point as "finish". It prints correctly when you do that, in combination with the changes in the code above:

Lua:
function LinkedList:print()
    if self.head.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
    end
    s = "["
    for node in self:loop(finish) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Alright, I tested it all and seems to work very well.
Just one little problem from the loop method. One expects "finish" node to be included in the execution of the loop, just like the "start" one is, however it is currently excluded. That seems a bit unintuitive.
You are right! I was originally thinking "if the head is the finish point, we skip it", but now with the below changes, the finish node will not be skipped if it is not the head:

Lua:
--[[
    Doubly-Linked List v1.5.2.0 by Wrda, Eikonium and Bribe, with special thanks to Jampion
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
LinkedList API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedListHead that can have nodes inserted to itself.

    list.next -> next item in the same list
    list.prev -> previous item in the same list

    list:insert([value : any, after : boolean]) -> LinkedListNode
    - Inserts *before* the given LinkedList object unless "after" is true.
    - If a value is passed, the system will attach it to the node as a generic "value"
    - Returns the inserted node that was added to the list.

    for node in LinkedList.loop(start : LinkedList[, finish : LinkedList , backwards : boolean]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    - An example of how to iterate while skipping the first and last nodes of the list:
        for node in list.next:loop(list.prev.prev) do print(node.value) end

API specific to LinkedListHead:
    list.remove(node)
    - Removes a node from list.

    list.n
    - The number of LinkedListNodes in the list.

    fromList:merge(intoList : LinkedList[, mergeBefore : boolean])
    - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
      at the beginning, if "mergeBefore" is true).
      "fromList" needs to be the linked list head, but "into" can be anywhere in that list.

LinkedListNode API:
    node:remove()
    - Removes a node from whatever list it is a part of.

    node.value
    - Whatever value might have been assigned from list:insert([value])
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList

---@class LinkedListNode : LinkedList
---@field value any

---@class LinkedListHead : LinkedList
---@field n integer

LinkedList = {}
LinkedList.__index = LinkedList

---Creates a new LinkedList head node.
---@return LinkedListHead new_list_head
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)

    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end

---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode new_node
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)

    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
 
    node.value = value

    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
end

---Removes a node from whatever list it is a part of.
function LinkedList:remove()
    self.prev.next = self.next
    self.next.prev = self.prev
    self.head.n = self.head.n - 1
end

---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore? boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
 
    for node in from:loop() do node.head = head end
    head.n = head.n + from.n
    from.n = 0
 
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into

    --reset the original list to a simple LinkedListHead
    from.next = from
    from.prev = from
end

---Loop a LinkedList from a starting node/head to a finish node/head in either direction.
---@param start LinkedList
---@param finish? LinkedList
---@param backward? boolean
---@return function
function LinkedList.loop(start, finish, backward)
    local head = start.head
    if head.n == 0 then return end
    local direction = backward and "prev" or "next"
    local skip = start ~= head
    if not finish or finish == head then
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
            end
            return start ~= head and start or nil
        end
    else
        return function()
            if start ~= finish then
                if skip then skip = nil
                else
                    start = start[direction]
                    if start == head then
                        start = start[direction]
                    end
                end
                return start
            end
        end
    end
end
--End of LinkedList library

@Jampion's code will have to be updated again to compensate for this change:

Lua:
function LinkedList:print()
    if self.head.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
    end
    s = "["
    for node in self:loop(finish.prev) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end

myList = LinkedList.create()
a = myList:insert(1)
b = myList:insert(2)
c = myList:insert(3)
myList:print()
a:print()
b:print()
c:print()
 
Last edited:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
If the start is the same as the finish, loop currently returns no elements.
a to b returns a and b
a to a returns no elements

This line seems to be the cause: if start ~= finish then
I think the easiest solution is just to add a special case for start == finish

Lua:
if not finish or finish == head then
   ...
else
   if finish == start then
      local r = true
      return function()
         if r then
            r = false
            return start
         end
      end
   else
      return function()
         ...
      end
   end
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
If the start is the same as the finish, loop currently returns no elements.
a to b returns a and b
a to a returns no elements

This line seems to be the cause: if start ~= finish then
I think the easiest solution is just to add a special case for start == finish

Lua:
if not finish or finish == head then
   ...
else
   if finish == start then
      local r = true
      return function()
         if r then
            r = false
            return start
         end
      end
   else
      return function()
         ...
      end
   end
end
Lua:
--[[
    Doubly-Linked List v1.5.2.2 by Wrda, Eikonium and Bribe, with special thanks to Jampion
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
LinkedList API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedListHead that can have nodes inserted to itself.

    list.next -> next item in the same list
    list.prev -> previous item in the same list

    list:insert([value : any, after : boolean]) -> LinkedListNode
    - Inserts *before* the given LinkedList object unless "after" is true.
    - If a value is passed, the system will attach it to the node as a generic "value"
    - Returns the inserted node that was added to the list.

    for node in LinkedList.loop(start : LinkedList[, finish : LinkedList , backwards : boolean]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    - An example of how to iterate while skipping the first and last nodes of the list:
        for node in list.next:loop(list.prev.prev) do print(node.value) end

API specific to LinkedListHead:
    list.remove(node)
    - Removes a node from list.

    list.n
    - The number of LinkedListNodes in the list.

    fromList:merge(intoList : LinkedList[, mergeBefore : boolean])
    - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
      at the beginning, if "mergeBefore" is true).
      "fromList" needs to be the linked list head, but "into" can be anywhere in that list.

LinkedListNode API:
    node:remove()
    - Removes a node from whatever list it is a part of.

    node.value
    - Whatever value might have been assigned from list:insert([value])
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList

---@class LinkedListNode : LinkedList
---@field value any

---@class LinkedListHead : LinkedList
---@field n integer

LinkedList = {}
LinkedList.__index = LinkedList

---Creates a new LinkedList head node.
---@return LinkedListHead new_list_head
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)

    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end

---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode new_node
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)

    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
 
    node.value = value

    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
end

---Removes a node from whatever list it is a part of.
function LinkedList:remove()
    self.prev.next = self.next
    self.next.prev = self.prev
    self.head.n = self.head.n - 1
end

---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore? boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
 
    for node in from:loop() do node.head = head end
    head.n = head.n + from.n
    from.n = 0
 
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into

    --reset the original list to a simple LinkedListHead
    from.next = from
    from.prev = from
end

---Loop a LinkedList from a starting node/head to a finish node/head in either direction.
---@param start LinkedList
---@param finish? LinkedList
---@param backward? boolean
---@return function
function LinkedList.loop(start, finish, backward)
    local head = start.head
    if head.n == 0 then return end
    local direction = backward and "prev" or "next"
    local skip = start ~= head or start == finish
    if not finish or finish == head then
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
            end
            return start ~= head and start or nil
        end
    else
        return function()
            if start ~= finish or skip then
                if skip and skip ~= 1 then
                    skip = start == finish and 1
                else
                    start = start[direction]
                    if start == head then
                        start = start[direction]
                    end
                    if start == finish then return end
                end
                return start
            end
        end
    end
end
--End of LinkedList library
Edit: hotfix to 1.5.2.2 to actually make it work.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've read about a way to "lubricate" a table for the garbage collector by setting the "__mode" metamethod to "v" or “k”. I've integrated that into the code below, so have now re-introduced the "destroy" method.

@Jampion @Wrda, I think this should satisfy the requirements and wishes at this point.

Lua:
--[[
    Doubly-Linked List v1.5.3.0 by Wrda, Eikonium and Bribe, with special thanks to Jampion
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
LinkedList API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedListHead that can have nodes inserted to itself.
    list.next -> next item in the same list
    list.prev -> previous item in the same list
    list:insert([value : any, after : boolean]) -> LinkedListNode
    - Inserts *before* the given LinkedList object unless "after" is true.
    - If a value is passed, the system will attach it to the node as a generic "value"
    - Returns the inserted node that was added to the list.
    for node in LinkedList.loop(start : LinkedList[, finish : LinkedList , backwards : boolean]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    - An example of how to iterate while skipping the first and last nodes of the list:
        for node in list.next:loop(list.prev.prev) do print(node.value) end
API specific to LinkedListHead:
    list.remove(node)
    - Removes a node from list.
    list.n
    - The number of LinkedListNodes in the list.
    fromList:merge(intoList : LinkedList[, mergeBefore : boolean])
    - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
      at the beginning, if "mergeBefore" is true).
      "fromList" needs to be the linked list head, but "into" can be anywhere in that list.
LinkedListNode API:
    node:remove()
    - Removes a node from whatever list it is a part of.
    node.value
    - Whatever value might have been assigned from list:insert([value])
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList
---@class LinkedListNode : LinkedList
---@field value any
---@class LinkedListHead : LinkedList
---@field n integer
LinkedList = {}
LinkedList.__index = LinkedList
LinkedList.destroyed = {__mode = "k"}
---Creates a new LinkedList head node.
---@return LinkedListHead new_list_head
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)
    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end
---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode new_node
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)
    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
 
    node.value = value
    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
end
--Transmute the list into something which can easily be garbage collected.
function LinkedList:destroy()
    setmetatable(self, LinkedList.destroyed)
end
--Removes a node from whatever list it is a part of.
function LinkedList:remove()
    self.prev.next = self.next
    self.next.prev = self.prev
    self.head.n = self.head.n - 1
    self:destroy()
end
---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore? boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
 
    for node in from:loop() do node.head = head end
    head.n = head.n + from.n
    from.n = 0
 
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into
    --reset the original list to a simple LinkedListHead
    from.next = from
    from.prev = from
end
---Loop a LinkedList from a starting node/head to a finish node/head in either direction.
---@param start LinkedList
---@param finish? LinkedList
---@param backward? boolean
---@return function
function LinkedList.loop(start, finish, backward)
    local head = start.head
    if head.n == 0 then return end
    local direction = backward and "prev" or "next"
    local skip = start ~= head or start == finish
    if not finish or finish == head then
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
            end
            return start ~= head and start or nil
        end
    else
        return function()
            if start ~= finish or skip then
                if skip and skip ~= 1 then
                    skip = start == finish and 1
                else
                    start = start[direction]
                    if start == head then
                        start = start[direction]
                    end
                    if start == finish then return end
                end
                return start
            end
        end
    end
end
--End of LinkedList library
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I remember about the "__mode" thing, but I've never really understood it. However, if you see that it does the job done, then certainly approved :D
__mode = "k" means the table's keys can be picked up by the garbage collector. __mode = "v" means the values can be picked up (more aggressive). I don't know the ins-and-outs of which one is better, or if there is any perceivable impact on how effective the garbage collection is on tables with such a metamethod, but it does at least change their metatable from LinkedList to that of a type that can be considered a destroyed instance.

Also, if you get a chance, please remove version 1.0 from your post (unless you still prefer it or are just not satisfied with the later versions). If you want a good demo script to play around with, I recommend the ones that @Jampion shared (with my edit):

Lua:
function LinkedList:print()
    if self.head.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
    end
    s = "["
    for node in self:loop(finish.prev) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end

myList = LinkedList.create()
a = myList:insert(1)
b = myList:insert(2)
c = myList:insert(3)
myList:print()
a:print()
b:print()
c:print()

The one method I like in the 1.0 version is the ForEach option. If your original ForEach was re-implemented, it would give a better performing alternative to the somewhat-bloated “loop”.
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
__mode = "k" means the table's keys can be picked up by the garbage collector. __mode = "v" means the values can be picked up (more aggressive). I don't know the ins-and-outs of which one is better, or if there is any perceivable impact on how effective the garbage collection is on tables with such a metamethod, but it does at least change their metatable from LinkedList to that of a type that can be considered a destroyed instance.

Also, if you get a chance, please remove version 1.0 from your post (unless you still prefer it or are just not satisfied with the later versions). If you want a good demo script to play around with, I recommend the ones that @Jampion shared (with my edit):

Lua:
function LinkedList:print()
    if self.head.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
    end
    s = "["
    for node in self:loop(finish.prev) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end

myList = LinkedList.create()
a = myList:insert(1)
b = myList:insert(2)
c = myList:insert(3)
myList:print()
a:print()
b:print()
c:print()

The one method I like in the 1.0 version is the ForEach option. If your original ForEach was re-implemented, it would give a better performing alternative to the somewhat-bloated “loop”.

I'm already satisfied with the final version. I only keep the first version there, which is deprecated, because I thought it would still be useful if some people like to see the history of it. Perhaps it's not worth at all having the first one there at all.
Hi @Wrda , I am waiting for feedback from @Jampion to see if this is up to standard. As I have had a hand in some of the recent edits, I don't want to have there be a conflict of interest /blind spot in me being the one to approve it.
Of course, perfectly understandable. I just wanted to grab some attention so it doesn't stay in here forever :p
 
Level 20
Joined
Jul 10, 2009
Messages
477
I've read about a way to "lubricate" a table for the garbage collector by setting the "__mode" metamethod to "v" or “k”. I've integrated that into the code below, so have now re-introduced the "destroy" method.
__mode = "k" means the table's keys can be picked up by the garbage collector. __mode = "v" means the values can be picked up (more aggressive). I don't know the ins-and-outs of which one is better, or if there is any perceivable impact on how effective the garbage collection is on tables with such a metamethod, but it does at least change their metatable from LinkedList to that of a type that can be considered a destroyed instance.

I suggest to remove that setting from the resource. Using the __mode-metamethod on a table T allows objects to get garbage collected even when there are still active references to them in T.

There are situations, where using the mode-setting is necessary to make an object garbage collectable at all and thus prevent memory leaks, but I don't see this being the case for Linked List.
Actually, allowing objects to get collected while being part of a Linked List sounds dangerous. If the garbage collection wasn't synced in Warcraft 1.32, this could even cause desyncs (and we can't be sure that garbage collection stays that way in future patches).
I know, you are applying this setting upon "destroying" the list, but at that point the user would stop to reference that LinkedList anyway, so its contents would be garbage collected anyway, making any help unnecessary.
Also, you are applying this to the LinkedListHead only (because the user would only use the destroy-method on the head), so it would only affect one single value (or none at all, because the head doesn't contain a value). Furthermore, the keys stored in a LinkedList-node are all strings ('next', 'prev', 'value', etc.), which are call-by-value, so the mode-setting has no effect on them anyway.

In short, you don't need to use it in this case. It doesn't have any performance implications. You typically only use it, when you want to make an object garbage collectable that otherwise could never be garbage collected. You typically use it directly after creating the table, not after destroying it. 'k' and 'v' need to be chosen carefully, otherwise you can even cause harm.

Hope, this helps :)

Best regards!
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Lua:
--[[
    Doubly-Linked List v1.5.4.0 by Wrda, Eikonium and Bribe, with special thanks to Jampion
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
    
LinkedList API:
    LinkedList.create() -> LinkedListHead
    - Creates a new LinkedListHead that can have nodes inserted to itself.
    
    list.next -> next item in the same list
    list.prev -> previous item in the same list
    
    list:insert([value : any, after : boolean]) -> LinkedListNode
    - Inserts *before* the given LinkedList object unless "after" is true.
    - If a value is passed, the system will attach it to the node as a generic "value"
    - Returns the inserted node that was added to the list.
    for node in LinkedList.loop(start : LinkedList[, finish : LinkedList , backwards : boolean]) do [stuff] end
    - Shows how to iterate over all nodes in "list".
    - An example of how to iterate while skipping the first and last nodes of the list:
        for node in list.next:loop(list.prev.prev) do print(node.value) end
        
API specific to LinkedListHead:
    list.remove(node)
    - Removes a node from list.
    list.n
    - The number of LinkedListNodes in the list.
    fromList:merge(intoList : LinkedList[, mergeBefore : boolean])
    - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
      at the beginning, if "mergeBefore" is true).
      "fromList" needs to be the linked list head, but "into" can be anywhere in that list.
      
LinkedListNode API:
    node:remove()
    - Removes a node from whatever list it is a part of.
    node.value
    - Whatever value might have been assigned from list:insert([value])
]]

---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList

---@class LinkedListNode : LinkedList
---@field value any

---@class LinkedListHead : LinkedList
---@field n integer

LinkedList = {}
LinkedList.__index = LinkedList

---Creates a new LinkedList head node.
---@return LinkedListHead new_list_head
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)
    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end

---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode new_node
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)
    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
 
    node.value = value
    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
end

--Nullify the linked list's inheritance (as a matter of principle).
function LinkedList:destroy()
    setmetatable(self, nil)
end

--Removes a node from whatever list it is a part of.
function LinkedList:remove()
    self.prev.next = self.next
    self.next.prev = self.prev
    self.head.n = self.head.n - 1
    self:destroy()
end

---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore? boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
 
    for node in from:loop() do node.head = head end
    head.n = head.n + from.n
    from.n = 0
 
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into
    --reset the original list to a simple LinkedListHead
    from.next = from
    from.prev = from
end

---Loop a LinkedList from a starting node/head to a finish node/head in either direction.
---@param start LinkedList
---@param finish? LinkedList
---@param backward? boolean
---@return function
function LinkedList.loop(start, finish, backward)
    local head = start.head
    if head.n == 0 then return end
    local direction = backward and "prev" or "next"
    local skip = start ~= head or start == finish
    if not finish or finish == head then
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
            end
            return start ~= head and start or nil
        end
    else
        return function()
            if start ~= finish or skip then
                if skip and skip ~= 1 then
                    skip = start == finish and 1
                else
                    start = start[direction]
                    if start == head then
                        start = start[direction]
                    end
                    if start == finish then return end
                end
                return start
            end
        end
    end
end
--End of LinkedList library
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
I tried the newest version and it works as expected. One minor thing, though. The loop function states that it loops to the finish node, but the finish node is not in the iteration. I think this behavior makes sense, but it should be stated clearly in the documentation.

I'd prefer if the user did not have to worry about the head node. It's more of an internal implementation detail that the user should not need be concerned with.

My current print function looks like this:
Lua:
function LinkedList:print()
    if self.head.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
    end
    s = "["
    for node in self:loop(finish) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end

I'd prefer if this part:
Lua:
if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
end
was not necessary.

I've experimented with the idea of having two separate objects: List and Node
There is no head inside the list, so there are no problems that prev or next might lead you to the head which you usually want to skip. But because there is not always a node in the list, you have to add other checks. So I'm not sure if it's of any use but here's the code anyway:

Lua:
do
    ---@class LinkedListNode
    LinkedListNode = {
        next = nil, ---@type LinkedListNode
        prev = nil, ---@type LinkedListNode
        value = nil, ---@type any
        list = nil ---@type LinkedList
    }
    LinkedListNode.__index = LinkedListNode
    ---@class LinkedList
    LinkedList = {
        head = nil, ---@type LinkedListNode
        n = 0,
    }
    LinkedList.__index = LinkedList
    ---Creates a new LinkedList.
    ---@return LinkedList new_list_head
    function LinkedList.create()
        local list = {}
        setmetatable(list, LinkedList)
        list.head = nil
        list.n = 0
        return list
    end
    ---Inserts *before* the given head/node, unless "backward" is true.
    ---@param value? any
    ---@param insertAfter? boolean
    ---@return LinkedListNode new_node
    function LinkedListNode:insert(value, insertAfter)
        local node = {} ---@type LinkedListNode
        setmetatable(node, LinkedListNode)
        local from = insertAfter and self.next or self
        from.prev.next = node
        node.prev = from.prev
        from.prev = node
        node.next = from
        node.value = value
        local list = from.list
        node.list = list
        list.n = list.n + 1
        return node
    end
    ---@param value? any
    ---@param front? boolean
    ---@return LinkedListNode new_node
    function LinkedList:insert(value, front)
        if self.n > 0 then
            return self.head:insert(value, front)
        else
            local node = {} ---@type LinkedListNode
            setmetatable(node, LinkedListNode)
            node.value = value
            node.list = self
            node.next = node
            node.prev = node
            self.n = 1
            self.head = node
            return node
        end
    end
    function LinkedListNode:remove()
        self.prev.next = self.next
        self.next.prev = self.prev
        local list = self.list
        list.n = list.n - 1
        if list.n > 0 then
            if self == list.head then
                list.head = self.next
            end
        else
            list.head = nil
        end
        self.list = nil
        self.next = nil
        self.prev = nil
        self.value = nil
    end
    ---@param into LinkedListNode | LinkedList
    function LinkedListNode:mergeInto(into)
        if into.head then
            into = into.head.prev
        end
        into = into ---@type LinkedListNode
        self.prev.next = into.next
        into.next.prev = self.prev
        self.prev = into
        into.next = self
        into.list.n = into.list.n + self.list.n
        self.list.n = 0
        self.list.head = nil
    end
    ---@param into LinkedListNode | LinkedList
    function LinkedList:mergeInto(into)
        self.head:mergeInto(into)
    end
    ---@param finish? LinkedListNode
    ---@param backward? boolean
    ---@return function
    function LinkedListNode:loop(finish, backward)
        local direction = backward and "prev" or "next"
        local current = self
        local done = finish ~= nil
        finish = finish or self
        return function()
            local tmp = current
            current = current[direction]
            if tmp == finish then
                if done then return else done = true end
            end
            return tmp
        end
    end
    ---@param finish? LinkedListNode
    ---@param backward? boolean
    ---@return function
    function LinkedList:loop(finish, backward)
        if not self.head then
            return function() end
        end
        if backward then
            return self.head.prev:loop(finish, backward)
        else
            return self.head:loop(finish, backward)
        end
    end
end
--End of LinkedList library


function LinkedListNode:print()
    if self.list.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    s = "["
    for node in self:loop(finish) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end
function LinkedList:print()
    if self.n == 0 then
        print("[]")
        return
    end
    self.head:print()
end

Another idea I had would be to have functions next() and prev() that skip over the head. However, you'd still have to keep in mind that the starting node could be the head.

With that said, even if the
Lua:
if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
end
part cannot be avoided, I think it's good enough for approval. I would however add some more explanations on how users should deal with the head node. An example would also be nice that shows that you need to check, whether something is the head and skip it if you want to access the values of the list.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
I tried the newest version and it works as expected. One minor thing, though. The loop function states that it loops to the finish node, but the finish node is not in the iteration. I think this behavior makes sense, but it should be stated clearly in the documentation.
Honestly, I would expect a loop from X to Y to be self inclusive.
I'd prefer if the user did not have to worry about the head node. It's more of an internal implementation detail that the user should not need be concerned with.
If you mean for the loops, yes I agree. I think the user should still have access of the node for the first and last node references.
My current print function looks like this:
Lua:
function LinkedList:print()
    if self.head.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
    end
    s = "["
    for node in self:loop(finish) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end

I'd prefer if this part:
Lua:
if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
end
was not necessary.

I've experimented with the idea of having two separate objects: List and Node
There is no head inside the list, so there are no problems that prev or next might lead you to the head which you usually want to skip. But because there is not always a node in the list, you have to add other checks. So I'm not sure if it's of any use but here's the code anyway:

Lua:
do
    ---@class LinkedListNode
    LinkedListNode = {
        next = nil, ---@type LinkedListNode
        prev = nil, ---@type LinkedListNode
        value = nil, ---@type any
        list = nil ---@type LinkedList
    }
    LinkedListNode.__index = LinkedListNode
    ---@class LinkedList
    LinkedList = {
        head = nil, ---@type LinkedListNode
        n = 0,
    }
    LinkedList.__index = LinkedList
    ---Creates a new LinkedList.
    ---@return LinkedList new_list_head
    function LinkedList.create()
        local list = {}
        setmetatable(list, LinkedList)
        list.head = nil
        list.n = 0
        return list
    end
    ---Inserts *before* the given head/node, unless "backward" is true.
    ---@param value? any
    ---@param insertAfter? boolean
    ---@return LinkedListNode new_node
    function LinkedListNode:insert(value, insertAfter)
        local node = {} ---@type LinkedListNode
        setmetatable(node, LinkedListNode)
        local from = insertAfter and self.next or self
        from.prev.next = node
        node.prev = from.prev
        from.prev = node
        node.next = from
        node.value = value
        local list = from.list
        node.list = list
        list.n = list.n + 1
        return node
    end
    ---@param value? any
    ---@param front? boolean
    ---@return LinkedListNode new_node
    function LinkedList:insert(value, front)
        if self.n > 0 then
            return self.head:insert(value, front)
        else
            local node = {} ---@type LinkedListNode
            setmetatable(node, LinkedListNode)
            node.value = value
            node.list = self
            node.next = node
            node.prev = node
            self.n = 1
            self.head = node
            return node
        end
    end
    function LinkedListNode:remove()
        self.prev.next = self.next
        self.next.prev = self.prev
        local list = self.list
        list.n = list.n - 1
        if list.n > 0 then
            if self == list.head then
                list.head = self.next
            end
        else
            list.head = nil
        end
        self.list = nil
        self.next = nil
        self.prev = nil
        self.value = nil
    end
    ---@param into LinkedListNode | LinkedList
    function LinkedListNode:mergeInto(into)
        if into.head then
            into = into.head.prev
        end
        into = into ---@type LinkedListNode
        self.prev.next = into.next
        into.next.prev = self.prev
        self.prev = into
        into.next = self
        into.list.n = into.list.n + self.list.n
        self.list.n = 0
        self.list.head = nil
    end
    ---@param into LinkedListNode | LinkedList
    function LinkedList:mergeInto(into)
        self.head:mergeInto(into)
    end
    ---@param finish? LinkedListNode
    ---@param backward? boolean
    ---@return function
    function LinkedListNode:loop(finish, backward)
        local direction = backward and "prev" or "next"
        local current = self
        local done = finish ~= nil
        finish = finish or self
        return function()
            local tmp = current
            current = current[direction]
            if tmp == finish then
                if done then return else done = true end
            end
            return tmp
        end
    end
    ---@param finish? LinkedListNode
    ---@param backward? boolean
    ---@return function
    function LinkedList:loop(finish, backward)
        if not self.head then
            return function() end
        end
        if backward then
            return self.head.prev:loop(finish, backward)
        else
            return self.head:loop(finish, backward)
        end
    end
end
--End of LinkedList library


function LinkedListNode:print()
    if self.list.n == 0 then
        print("[]")
        return
    end
    finish = self.prev -- last node to print
    s = "["
    for node in self:loop(finish) do
        s = s .. (node.value) .. ", "
    end
    s = s .. finish.value .. "]"
    print(s)
end
function LinkedList:print()
    if self.n == 0 then
        print("[]")
        return
    end
    self.head:print()
end

Another idea I had would be to have functions next() and prev() that skip over the head. However, you'd still have to keep in mind that the starting node could be the head.

With that said, even if the
Lua:
if finish.head == finish then -- if it is the head, use previous node
        finish = finish.prev
end
part cannot be avoided, I think it's good enough for approval. I would however add some more explanations on how users should deal with the head node. An example would also be nice that shows that you need to check, whether something is the head and skip it if you want to access the values of the list.
Seems fair enough.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
It would be cool add the methods "dequeue()" and "pop()" to trait the LinkedList as a Queue or a Stack respectively, also because this don't have a clean way to get and remove the first or last value of the list, maybe by doing:
Lua:
-- Dequeue
node = list.next
-- Pop
node = list.prev

value = node.value
node:remove()
But the system could have it by default.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
Implemented next() and prev() functions to skip over the head. Loop function range limits are both inclusive.
While the head is more of an internal implementation, it is still useful to get the head of the node when needed.
Lua:
--[[
    Doubly-Linked List v1.5.4.1 by Wrda, Eikonium and Bribe, with special thanks to Jampion
    ------------------------------------------------------------------------------
    A script that enables linking objects together with "previous" and "next"
    syntax.
    ------------------------------------------------------------------------------
    
LinkedList API:
    LinkedList.create() -> LinkedListHead
      - Creates a new LinkedListHead that can have nodes inserted to itself.
    list.head -> the head (internal implementation) of the list. [node.head] has the same result.
    list.next -> next item in the same list. [list.next] is always the first node of the list.
                 Doesn't skip the head.
    list.prev -> previous item in the same list. [list.prev] is always the last node of the list.
                 Doesn't skip the head.
    
    list:insert([value : any, after : boolean]) -> LinkedListNode
      - Inserts *before* the given LinkedList object unless "after" is true.
      - If a value is passed, the system will attach it to the node as a generic "value"
      - Returns the inserted node that was added to the list.
    for node in LinkedList.loop(start : LinkedList [, finish : LinkedList , backwards : boolean]) do [stuff] end
      - Shows how to iterate over all nodes in "list". Inclusive range.
      - An example of how to iterate while skipping the first and last nodes of the list:
        for node in list.next.next:loop(list.prev.prev) do print(node.value) end
        
API specific to LinkedListHead:
    list.remove(node)
      - Removes a node from list.
    list.n
      - The number of LinkedListNodes in the list.
    fromList:merge(intoList : LinkedList [, mergeBefore : boolean])
      - Removes all nodes of list "fromList" and adds them to the end of list "intoList" (or
        at the beginning, if "mergeBefore" is true).
        "fromList" needs to be the linked list head, but "into" can be anywhere in that list.
      
LinkedListNode API:
    node:remove()
      - Removes a node from whatever list it is a part of.
    node:getNext()
      - Gets the next node, ignoring the head.
    node:getPrev()
      - Gets the previous node, ignoring the head.
    node.value
      - Whatever value might have been assigned from list:insert([value])
]]
---@class LinkedList     : table
---@field head LinkedListHead
---@field next LinkedList
---@field prev LinkedList
---@class LinkedListNode : LinkedList
---@field value any
---@class LinkedListHead : LinkedList
---@field n integer
LinkedList = {}
LinkedList.__index = LinkedList
---Creates a new LinkedList head node.
---@return LinkedListHead new_list_head
function LinkedList.create()
    local head = {}
    setmetatable(head, LinkedList)
    head.next = head
    head.prev = head
    head.head = head
    head.n = 0
    return head
end
---Inserts *before* the given head/node, unless "backward" is true.
---@param value? any
---@param insertAfter? boolean
---@return LinkedListNode new_node
function LinkedList:insert(value, insertAfter)
    local node = {}   ---@type LinkedListNode
    setmetatable(node, LinkedList)
    local from = insertAfter and self.next or self
    from.prev.next = node
    node.prev = from.prev
    from.prev = node
    node.next = from
 
    node.value = value
    local head = from.head
    node.head = head
    head.n = head.n + 1
    return node
end
---Removes a node from whatever list it is a part of.
function LinkedList:remove()
    self.prev.next = self.next
    self.next.prev = self.prev
    self.head.n = self.head.n - 1
    self:destroy()
end
---Nullify the linked list's inheritance (as a matter of principle).
function LinkedList:destroy()
    setmetatable(self, nil)
end
--Gets the next node in the sequence of the list it is in, ignoring the head.
function LinkedList:getNext()
    if self.next == self.head then
        return self.head.next
    end
    return self.next
end
--Gets the previous node in the sequence of the list it is in, ignoring the head.
function LinkedList:getPrev()
    if self.prev == self.head then
        return self.head.prev
    end
    return self.prev
end

---Merges LinkedListHead "from" to a LinkedList "into"
---@param from LinkedListHead
---@param into LinkedList
---@param mergeBefore? boolean
function LinkedList.merge(from, into, mergeBefore)
    local head = into.head
    into = mergeBefore and into.next or into
 
    for node in from:loop() do node.head = head end
    head.n = head.n + from.n
    from.n = 0
 
    from.next.prev = into.prev
    into.prev.next = from.next
    into.prev = from.prev
    from.prev.next = into
    --reset the original list to a simple LinkedListHead
    from.next = from
    from.prev = from
end
---Loop a LinkedList from a starting node/head to a finish node/head in either direction.
---Inclusive range.
---@param start LinkedList
---@param finish? LinkedList
---@param backward? boolean
---@return function
function LinkedList.loop(start, finish, backward)
    local head = start.head
    if head.n == 0 then return end
    local direction = backward and "prev" or "next"
    local skip = start ~= head or start == finish
    if not finish or finish == head then
        return function()
            if skip then
                skip = nil
            else
                start = start[direction]
            end
            return start ~= head and start or nil
        end
    else
        return function()
            if start ~= finish or skip then
                if skip and skip ~= 1 then
                    skip = start == finish and 1
                else
                    start = start[direction]
                    if start == head then
                        start = start[direction]
                    end
                end
                return start
            end
        end
    end
end
--End of LinkedList library
It would be cool add the methods "dequeue()" and "pop()" to trait the LinkedList as a Queue or a Stack respectively, also because this don't have a clean way to get and remove the first or last value of the list, maybe by doing:
Lua:
-- Dequeue
node = list.next
-- Pop
node = list.prev

value = node.value
node:remove()
But the system could have it by default.
I don't know what a LinkedList is if not a "queue" or a "stack". By definition, it's a sequence of nodes. Perhaps it wasn't explained very well how to get the first and last nodes before, but I hope I've cleared that up with this update.
So simply this will do:
Lua:
--get first node
first = list.next

--if you don't have a reference for the list then:
head = first.head -- or any other node
node:remove()
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
A great linked list implementation. Looping over the list is very convenient and all functions are implemented efficiently. Approved.
 
Top