Fusion icon indicating copy to clipboard operation
Fusion copied to clipboard

New table function, ForRange

Open TaylorsRus opened this issue 9 months ago • 8 comments

Fusion is currently lacking a function which covers for Count = 1, X do. Alternatives we're stuck with look something like this

Scope:Computed(function()
    local Table = {}
    for Count = 1, 1, 1 do
        table.insert(Table, Scope:New("")({})
    end
end)

Thus, im proposing syntax along these lines, to serve as utility, while also allowing fusion to add optimizations which a Computed can currently not serve, such as not recreating Children when a new one is added, since currently a computed would recreate every instance if the End state was turned from say, 1 to 2.

Scope:ForRange({Start: number, End: number, Increment: number?}, function(Current: number)
    return Scope:New("")({})    
end)

TaylorsRus avatar Mar 10 '25 18:03 TaylorsRus

I'm prepared to make a PR for this after some people have looked at it and made sure I'm not saying anything abhorrently stupid, and also ensured no improvements can be made to it

TaylorsRus avatar Mar 10 '25 18:03 TaylorsRus

Why not just have number arguments for start, end, and step, like how Spring hasit?

znotfireman avatar Mar 10 '25 23:03 znotfireman

Why not just have number arguments for start, end, and step, like how Spring hasit?

Reason being, Increment is a niche use case, optional argument, which will be silenced 95% of the time, and since the function is the last argument but is non-optional, you'd be silencing the Increment almost every call, which seems unintuitive to me

TaylorsRus avatar Mar 11 '25 00:03 TaylorsRus

Can't you just use a ForValues with a number array?

local function range(start, ending, step)
    local arr = {}
    -- i forgot the syntax but
    for i = start, ending, step do
        table.insert(arr, i)
    end
    return arr
end

scope:ForValues(range(1, 10), function(use, scope, i)
    -- ...
end))

znotfireman avatar Mar 12 '25 13:03 znotfireman

Can't you just use a ForValues with a number array?

local function range(start, ending, step) local arr = {} -- i forgot the syntax but for i = start, ending, step do table.insert(arr, i) end return arr end

scope:ForValues(range(1, 10), function(use, scope, i) -- ... end))

In response, can you not just use any combination of ForValues, ForKeys and ForPairs, to make any combination of each other?

It's purposefully tagged with enhancement. The original post points out that it's possible, but not natively, where it should be possible natively following the trend of the other iteration functions

TaylorsRus avatar Mar 12 '25 14:03 TaylorsRus

Can't you just use a ForValues with a number array?

local function range(start, ending, step) local arr = {} -- i forgot the syntax but for i = start, ending, step do table.insert(arr, i) end return arr end

scope:ForValues(range(1, 10), function(use, scope, i) -- ... end))

I would argue that this is too much of unnecessary performance overhead, both time and memory wise. Time because you are going to iterate twice, first time to fill the table, and second time through ForValues. Memory because you are allocating space for an array that will be filled with 64-bit floating point numbers. Although the performance may be negligible, it does stack, because developer definitely won't have just a simple list of 50 numbers in a product (e.g., game, plugin). The time and memory can be saved for something else that is more important. Additionally, it will be one less utility function to write for a developer. By having a dedicated method we cut down on unnecessary performance overhead and inconvenience. I personally found this useful for prototyping UI where I didn't yet have data to fill up my list or grid.

melindatrace avatar Mar 12 '25 16:03 melindatrace

Reason being, Increment is a niche use case, optional argument, which will be silenced 95% of the time, and since the function is the last argument but is non-optional, you'd be silencing the Increment almost every call, which seems unintuitive to me

I'm not sure if overloaded functions are against Fusion's principles, but ForRange could be written like this:

type ForRangeCallback<T> = (use: Use, scope: Scope, iteration: number) -> T
type ForRangeConstructor<T> =
	& (scope: Scope, start: number, stop: number, callback: ForRangeCallback<T>) -> ForRange
	& (scope: Scope, start: number, stop: number, increment: number, callback: ForRangeCallback<T>) -> ForRange

local ForRange = function(...)
	local result = {}
	if select("#", ...) == 4 then
		local scope, start, stop, callback = ...
		for i = start, stop do
			-- ...
		end
	elseif select("#", ...) == 5 then
		local scope, start, stop, increment, callback = ...
		for i = start, stop, increment do
			-- ...
		end
	end
	return result
end :: ForRangeConstructor

This would allow ranges to be specified more tersely without the need to pass nil for increment in most cases.


One example use case for ForRange would be generating a list of children. An alternative I found for this case is to write an anonymous function inline, and then call it. However, it isn't pretty and requires some imperative code.

scope:New "Frame" {
	[Children] = {
		scope:New "UIListLayout" {},

		(function()
			local labels = {}
			for i = 1, 10 do
				labels[i] = scope:New "TextLabel" {
					Text = `Item {i}`,
				}
			end
			return labels
		end)(),
	},
}

Here's how I'd imagine it would look with ForRange:

scope:New "Frame" {
	[Children] = {
		scope:New "UIListLayout" {},

		scope:ForRange(1, 10, function(use, scope, i)
			return scope:New "TextLabel" {
				Text = `Item {i}`,
			}
		end),
	},
}

Great-Bird avatar Apr 22 '25 20:04 Great-Bird

Definitely support this, a state object like ForRange would be so useful to have in Fusion. My main question, however, is would this use For/Disassembly under the hood? or would it use it's own custom logic for handling reusing values? I know that Disassembly only really works with tables that have already been populated, so I would assume the latter if we really to avoid any potential performance overheads.


I've already experimented with implementing this myself, with support for UsedAs values, as a ForValues wrapper and it works pretty well. This works fine for me, since I'm not too worried about performance.

local function ForRange<S, T>(
	scope: memory.Scope<S>,
	start: calc.UsedAs<number>,
	finish: calc.UsedAs<number>,
	processor: iter.ForRangeProcessor<S, T>,
	step: calc.UsedAs<number>?
): iter.For<number, T>
	local new_range = Range(scope, start, finish, step)

	return ForValues(scope, new_range, processor)
end

I've opted to put the step parameter after the processor, since I feel like overloading functions can be quite cumbersome and can lead to code increasing in complexity very quickly. Usually if a function behaves differently based off of the number/order/type of arguments passed to it, I would opt to instead create separate functions to handle those cases, instead of doing loads of checks to change the flow of code execution.

HappySunChild avatar Nov 01 '25 02:11 HappySunChild