Module:FunList/Sandbox/doc: Difference between revisions

From Melvor Idle
No edit summary
No edit summary
Line 427: Line 427:
=== reduce ===
=== reduce ===
Alias for [[#aggregate|Enumerator3.aggregate]]
Alias for [[#aggregate|Enumerator3.aggregate]]
== Module Explanation ==
<syntaxhighlight lang="lua">
--------------------------------------------------------------------------------
-- Explanation on how this module functions / how it can be extended
--------------------------------------------------------------------------------
----------------------------------------------------
-- Chaining functions and the Enumerator3 class
----------------------------------------------------
--[[
The Enumerator3 class holds metadata for chaining functions and the context for the operation. Specifically, it holds three pieces of information:
- The function to be executed next in the sequence.
- Index of the Iterator Function: (which is 0 for ipairs and nil for pairs)
- The Context Factory (A function that generates a fresh context)
The Enumerator3 class ONLY serves as a storage for metadata with the sole purpose of chaining functions. This is also the reason why all chainable functions are added to Enumerator3. When a chainable function gets called, a new Enumerator3 instance gets created. The current (self) Enumerator3 metadata is passed to the newly created instance to form a chain (via the context object). The Enumerator3 class is ONLY used when functions are chained, NOT when executed. This is also why this module is as fast as it is. There are no calls made to retrieve data from the Enumerator3 instances when iterating. The context object is exclusively used for this purpose.
When the chain of functions gets evaluated, the following happens.
1. The next function in the chain is retrieved.
2. The context object for the next function is created via the factory.
3. The index for the next function is retrieved.
4. The next function is invoked using the context and index. (Usually in a loop, until the index returns nil)
The above 4 steps happen for every function in the chain.
]]
----------------------------------------------------
-- The context object and context factory
----------------------------------------------------
--[[
Since some functions require data to be remembered for consecutive "nextFunc" calls, a context object is required. This can be seen, for example, for the map function. Where the map function stores an index (position) and passes it to the selector function.
However, if the Enumerator3 object is reused or chained with a different function, the context from the previous iteration persists, leading to incorrect behavior. For example, in the map function, the index remains at the last iteration's position instead of restarting at 0. To prevent this, the context isn't passed directly to Enumerator3. Instead, it utilizes a function, known as a context factory, to generate a fresh context each time the function chain is evaluated.
This is achieved via the wrapContext function. The context always contains the same fields in the same order:
[1] State: Represents the current state of the function.
[2] Next Function: The subsequent function in the chain.
[3] Next Context (Factory): The factory function for creating the next context.
Depending on the specific function used, additional fields may be added to the context object. For example, the map function appends:
- A selector function as the 4th field.
- An indexer as the 5th field.
The sequence in which fields are added is crucial because it determines how they must be retrieved later. The fields added by a chainable function must align precisely with the fields expected by its implementation function.
For example in the map (chainable) function we see:
"Enumerator3.new(mapFunc, self.index, wrapContext2(self, selector, 0))"
The wrapContext function adds the initial three fields (state, next function, next context factory), with the selector as the 4th field and the indexer (defaulting to 0) as the 5th field.
The implementing function of map retrieves the fields in the exact order they were added:
Example: The implementation of map retrieves all of these fields in the same order when called:
    local state = context[1]
    local nextFunc = context[2]
    local nextContext = context[3]
    local selector = context[4]
    local position = context[5]
Maintaining this order is critical for ensuring that the context is correctly interpreted and that each function receives the data it expects.
]]
----------------------------------------------------
-- The state of the implementing function
----------------------------------------------------
--[[
When an implementing function is invoked for the first time, the context object must be created using the provided factory function. During this initial call, the following occurs:
- Check Initial State: The state is checked to see if it is 0.
- Replace Context Factory: If the state is 0, the context factory function is replaced with the context itself via the initialiseContext function.
- Set to Active State: The state is then set to 1, signifying that the function is now in its active iteration phase.
On subsequent calls to the implementing function, the initialization step (state 0) is bypassed, and the function proceeds directly with the iteration step. Some implementing functions may require multiple states to handle more complex behaviors.
When the state becomes -4, it signifies that the iteration has reached its end. At this point, the function returns nil to indicate to the caller that there are no further elements to process.
]]
----------------------------------------------------
-- Expanding upon the module
----------------------------------------------------
--[[
If a new chainable function gets added (a function that returns an Enumerator3 instance) the following should be ensured:
1. A chainable function and an implementation function pair must be implemented.
2. The chainable function must be added to the Enumerator3 class to allow chaining.
3. The chainable function must pass a context factory to the Enumerator3 constructor, with the parameters in the same order as the implementing function uses them.
4. The implementing function must retrieve the required fields in the same order as the context factory specifies.
5. The implementing function must create a new context via the factory function and store the context for future iterations.
6. The state field can be used to track the state of the implementing function and change its behaviour. For instance, having an initialisation state and an iteration state.
Simple functions such as the map/mapFunc and where/whereFunc can be used as a reference.
If a new result function gets added (a function that iterates over the function chain and returns a direct result) the following should be ensured:
1. Retrieve the nextFunc, contextFactory and index from the current object (self).
2. Create the context by calling the contextFactory.
3. Iterate over the chain by calling nextFunc using the context and index.
If the index returned from nextFunc is nil, it means that the iteration has come to an end.
]]
</syntaxhighlight>

Revision as of 15:40, 15 August 2024

Functions

new

---@param nextFunc fun(context: any, index: any):any, any
---@param index any
---@param contextFactory fun(): any
---@return Enumerator3
function Enumerator3.new(nextFunc, index, contextFactory)

create

---@param obj any
---@return Enumerator3
function Enumerator3.create(obj)

ipairs

---@return fun():table
function Enumerator3:ipairs()

where

---Filters a sequence of values based on a predicate.
---@param predicate fun(item: any, index: any): boolean
---@return Enumerator3
function Enumerator3:where(predicate)

map

---Projects each element of a sequence into a new form.
---@param selector fun(item: any, position: integer): any
---@return Enumerator3
function Enumerator3:map(selector)

flatMap

---Projects each element of a sequence and flattens the resulting sequences into one sequence.
---@param selector fun(item: any, position: integer): table
---@return Enumerator3
function Enumerator3:flatMap(selector)

concat

---Concatenates two sequences.
---@param second table
---@return Enumerator3
function Enumerator3:concat(second)

append

---Appends a value to the end of the sequence.
---@param item any
---@param index? any
---@return Enumerator3
function Enumerator3:append(item, index)

prepend

---Prepends a value to the start of the sequence.
---@param item any
---@param index? any
---@return Enumerator3
function Enumerator3:prepend(item, index)

unique

---Returns unique (distinct) elements from a sequence according to a specified key selector function.
---@param keySelector? fun(current: any, index: any): any
---@return Enumerator3 
function Enumerator3:unique(keySelector)

difference

---Produces the set difference of two sequences according to a specified key selector function.
---@param second table
---@param keySelector? fun(current: any, index: any): any
---@return Enumerator3
function Enumerator3:difference(second, keySelector)

union

---Produces the set union of two sequences according to a specified key selector function.
---@param second table
---@param keySelector? fun(current: any, index: any): any
---@return Enumerator3
function Enumerator3:union(second, keySelector)

intersect

---Produces the set intersection of two sequences according to a specified key selector function.
---@param second table
---@param keySelector? fun(current: any, index: any): any
function Enumerator3:intersect(second, keySelector)

zip

---Applies a specified function to the corresponding elements of two sequences, producing a sequence of the results.
---@param second table
---@param resultSelector? fun(itema: any, itemb: any): any
---@return Enumerator3
function Enumerator3:zip(second, resultSelector)

groupBy

---Groups the elements of a sequence.
---@param keySelector fun(param: any): any
---@param elementSelector? fun(param: any): any
---@return Enumerator3
function Enumerator3:groupBy(keySelector, elementSelector)

groupByResult

---Groups the elements of a sequence.
---@param keySelector fun(param: any): any
---@param elementSelector? fun(param: any): any
---@param resultSelector fun(key: any, elements: any): any
---@return Enumerator3
function Enumerator3:groupByResult(keySelector, elementSelector, resultSelector)

sort

---Boilerplate function to setup various sorting Enumerator3
---@param self Enumerator3
---@param isDecending boolean
---@param selector? fun(current: any): any
---@return Enumerator3
local function getSorter(self, isDecending, selector)

sortDescending

function Enumerator3:sortDescending()

sortBy

function Enumerator3:sortBy(selector)

sortByDescending

function Enumerator3:sortByDescending(selector)

thenBy

function Enumerator3:thenBy(selector)

thenByDecending

function Enumerator3:thenByDecending(selector)

indexOf

---Determines the index of the first occurrence of a specified item.
---@param self Enumerator3
---@param item any
---@param comparer? fun(a: any, b: any): boolean
---@return integer
function Enumerator3.indexOf(self, item, comparer)

contains

---Determines whether the sequence contains the provided item according to an equality comparer.
---@param self table
---@param item any
---@param comparer? fun(a: any, b: any): boolean
---@return boolean
function Enumerator3.contains(self, item, comparer)

sequenceEquals

---Determines whether two sequences are equal according to an equality comparer.
---@param self table
---@param other table
---@param comparer? fun(a: any, b: any): boolean
---@return boolean
function Enumerator3.sequenceEquals(self, other, comparer)

firstOrDefault

---Returns the first item in the sequence, or a default value if there are no items in the sequence.
---@param self table
---@param predicate? fun(value: any, key: any): boolean
---@param defaultValue? any
---@return any
function Enumerator3.firstOrDefault(self, predicate, defaultValue)

first

---Returns the first item in the sequence, or an error if there are no items in the sequence.
---@param self table
---@param predicate? fun(value: any, key: any): boolean
---@return any
function Enumerator3:first(predicate)

lastOrDefault

---Returns the last item in the sequence, or a default value if there are no items in the sequence.
---@param self table
---@param predicate? fun(value: any, key: any): boolean
---@param defaultValue? any
---@return any
function Enumerator3:lastOrDefault(predicate, defaultValue)

last

---Returns the last item in the sequence, or an error if there are no items in the sequence.
---@param self table
---@param predicate? fun(value: any, key: any): boolean
---@return any
function Enumerator3:last(predicate)

count

---Returns the length of this collection.
---@param self table
---@return integer
function Enumerator3:count()

countBy

---Returns the number of elements in a sequence that match a predicate.
---@param self Enumerator3
---@param predicate fun(value: any, key: any): any
---@return integer
function Enumerator3:countBy(predicate)

all

---Determines whether all elements of a sequence satisfy a condition.
---@param self Enumerator3
---@param predicate fun(current: any, index: any): boolean
---@return boolean
function Enumerator3:all(predicate)

any

---Determines whether any element of a sequence exists or satisfies a condition.
---@param self Enumerator3
---@param predicate? fun(current: any, index: any): boolean
---@return boolean
function Enumerator3:any(predicate)

foreach

---Iterates over the sequence, applying a specified function to each element.
---@param self Enumerator3
---@param func fun(current: any): any
---@return nil
function Enumerator3:foreach(func)

aggregate

---Applies an accumulator function over a sequence. The specified seed value is used as the initial accumulator value, and the specified function is used to select the result value.
---@param self table
---@param func fun(accumulate: any, next: any): any
---@param seed? any
---@param resultSelector? fun(result: any): any
---@return any
function Enumerator3:aggregate(func, seed, resultSelector)

max

---Returns the maximum value in a sequence of values.
---@param self Enumerator3
---@return number
function Enumerator3:max()

maxBy

---Returns the item with the highest value in a sequence.
---@param self Enumerator3
---@param selector fun(item: any, index: any): number
---@return unknown
function Enumerator3:maxBy(selector)

min

---Returns the minimum value in a sequence of values.
---@param self Enumerator3
---@return number
function Enumerator3:min()

minBy

---Returns the item with the lowest value in a sequence.
---@param self Enumerator3
---@param selector fun(item: any, index: any): number
---@return unknown
function Enumerator3:minBy(selector)

sum

---Returns the sum value of a sequence of values.
---@param self Enumerator3
---@return number
function Enumerator3:sum()

toSet

---Creates a set from a table|Enumerator3 with a specified key for uniqueness.
---@param self table
---@param keySelector? fun(current: any, index: any): any
---@return table
function Enumerator3:toSet(keySelector)

toTable

---Creates an array from a table|Enumerator3
---@param self table
---@return table
function Enumerator3:toTable()

toLookup

---Creates a lookup from a table|Enumerator3
---This is a { key, { elements } } structure
---@param self table
---@param keySelector fun(value: any, index: any): any
---@param elementSelector? fun(value: any, index: any): any
---@return table
function Enumerator3:toLookup(keySelector, elementSelector)

toDictionary

---Creates a dictionary (key, value) structure based on a keyselector and an elementselector
---@param self Enumerator3
---@param keySelector? fun(value: any, key: any): any
---@param elementSelector? fun(value: any, key: any): any
---@return table
function Enumerator3:toDictionary(keySelector, elementSelector)

deepCopy

---Creates a deep copy of the provided object.
---@param self any
---@return any
function Enumerator3:deepCopy()

Function aliases

These aliases behave the same as the functions listed above. They only have a different name.

select

Alias for Enumerator3.map

selectMany

Alias for Enumerator3.flatMap

distinct

Alias for Enumerator3.unique

except

Alias for Enumerator3.difference

order

Alias for Enumerator3.sort

orderDecending

Alias for Enumerator3.sortDescending

orderBy

Alias for Enumerator3.sortBy

orderByDecending

Alias for Enumerator3.sortByDescending

each

Alias for Enumerator3.foreach

for_each

Alias for Enumerator3.foreach

length

Alias for Enumerator3.count

lengthBy

Alias for Enumerator3.countBy

has

Alias for Enumerator3.contains

fold

Alias for Enumerator3.aggregate

reduce

Alias for Enumerator3.aggregate

Module Explanation

--------------------------------------------------------------------------------
-- Explanation on how this module functions / how it can be extended
--------------------------------------------------------------------------------

----------------------------------------------------
-- Chaining functions and the Enumerator3 class
----------------------------------------------------
--[[
The Enumerator3 class holds metadata for chaining functions and the context for the operation. Specifically, it holds three pieces of information:
- The function to be executed next in the sequence.
- Index of the Iterator Function: (which is 0 for ipairs and nil for pairs)
- The Context Factory (A function that generates a fresh context)

The Enumerator3 class ONLY serves as a storage for metadata with the sole purpose of chaining functions. This is also the reason why all chainable functions are added to Enumerator3. When a chainable function gets called, a new Enumerator3 instance gets created. The current (self) Enumerator3 metadata is passed to the newly created instance to form a chain (via the context object). The Enumerator3 class is ONLY used when functions are chained, NOT when executed. This is also why this module is as fast as it is. There are no calls made to retrieve data from the Enumerator3 instances when iterating. The context object is exclusively used for this purpose.

When the chain of functions gets evaluated, the following happens.
1. The next function in the chain is retrieved.
2. The context object for the next function is created via the factory.
3. The index for the next function is retrieved.
4. The next function is invoked using the context and index. (Usually in a loop, until the index returns nil)

The above 4 steps happen for every function in the chain.
]]

----------------------------------------------------
-- The context object and context factory
----------------------------------------------------
--[[
Since some functions require data to be remembered for consecutive "nextFunc" calls, a context object is required. This can be seen, for example, for the map function. Where the map function stores an index (position) and passes it to the selector function. 

However, if the Enumerator3 object is reused or chained with a different function, the context from the previous iteration persists, leading to incorrect behavior. For example, in the map function, the index remains at the last iteration's position instead of restarting at 0. To prevent this, the context isn't passed directly to Enumerator3. Instead, it utilizes a function, known as a context factory, to generate a fresh context each time the function chain is evaluated.

This is achieved via the wrapContext function. The context always contains the same fields in the same order:
[1] State: Represents the current state of the function.
[2] Next Function: The subsequent function in the chain.
[3] Next Context (Factory): The factory function for creating the next context.

Depending on the specific function used, additional fields may be added to the context object. For example, the map function appends:
- A selector function as the 4th field.
- An indexer as the 5th field.
The sequence in which fields are added is crucial because it determines how they must be retrieved later. The fields added by a chainable function must align precisely with the fields expected by its implementation function.

For example in the map (chainable) function we see:
"Enumerator3.new(mapFunc, self.index, wrapContext2(self, selector, 0))"

The wrapContext function adds the initial three fields (state, next function, next context factory), with the selector as the 4th field and the indexer (defaulting to 0) as the 5th field.

The implementing function of map retrieves the fields in the exact order they were added:
Example: The implementation of map retrieves all of these fields in the same order when called:
    local state = context[1]
    local nextFunc = context[2]
    local nextContext = context[3]
    local selector = context[4]
    local position = context[5]


Maintaining this order is critical for ensuring that the context is correctly interpreted and that each function receives the data it expects.
]]


----------------------------------------------------
-- The state of the implementing function
----------------------------------------------------
--[[
When an implementing function is invoked for the first time, the context object must be created using the provided factory function. During this initial call, the following occurs:

- Check Initial State: The state is checked to see if it is 0.
- Replace Context Factory: If the state is 0, the context factory function is replaced with the context itself via the initialiseContext function.
- Set to Active State: The state is then set to 1, signifying that the function is now in its active iteration phase.

On subsequent calls to the implementing function, the initialization step (state 0) is bypassed, and the function proceeds directly with the iteration step. Some implementing functions may require multiple states to handle more complex behaviors. 

When the state becomes -4, it signifies that the iteration has reached its end. At this point, the function returns nil to indicate to the caller that there are no further elements to process. 
]]

----------------------------------------------------
-- Expanding upon the module
----------------------------------------------------
--[[
If a new chainable function gets added (a function that returns an Enumerator3 instance) the following should be ensured:
1. A chainable function and an implementation function pair must be implemented.
2. The chainable function must be added to the Enumerator3 class to allow chaining.
3. The chainable function must pass a context factory to the Enumerator3 constructor, with the parameters in the same order as the implementing function uses them.
4. The implementing function must retrieve the required fields in the same order as the context factory specifies.
5. The implementing function must create a new context via the factory function and store the context for future iterations.
6. The state field can be used to track the state of the implementing function and change its behaviour. For instance, having an initialisation state and an iteration state.

Simple functions such as the map/mapFunc and where/whereFunc can be used as a reference.



If a new result function gets added (a function that iterates over the function chain and returns a direct result) the following should be ensured:
1. Retrieve the nextFunc, contextFactory and index from the current object (self).
2. Create the context by calling the contextFactory.
3. Iterate over the chain by calling nextFunc using the context and index.

If the index returned from nextFunc is nil, it means that the iteration has come to an end.
]]