What's the point of "<>" in function declaration?

Hi, saw this post with a code snippet where a function was declared like this:

function map<T, U, V...>(callback : (T, V...) -> (U), tbl : Array<T>, ... : V...) : () -> U?

What does <T, U, V> mean? Thanks!

2 Likes

Anything inside of those refers to a “generic” type. I’m going to assume that you understand the concept of --!strict, but if you don’t then let me know and I’ll make an edit.

Once in a while it is useful to have a function that can take different types instead of just one. Take this for example:

local function AddTwoVector3s(a: Vector3, b: Vector3): Vector3
    return a + b
end
local function AddTwoNumbers(a: number, b: number): number
    return a + b
end

In both cases, the code inside is the same. They also both return the same type as the parameters. We can re-write these functions using generics:

local function AddTwo<T>(a: T, b: T): T
    return a + b
end

This function can operate on any type. T is kinda like a variable which represents that type. So both a and b can be any type, so long as they are the same type. The function returns the same type that a and b are. So what if we want to add two different types? i.e. CFrame + Vector3? We need another generic type.

local function AddTwo<T, U>(a: T, b: U): T
    return a + b
end

This takes two parameters of any type (referred to as type “T” and type “U”) and returns the same type that the first value is. This works with CFrames like so

AddTwo(CFrame.new(1, 2, 3), Vector3.new(4, 5, 6)) -- valid

And since adding a CFrame to a Vector3 gives you a CFrame, the function correctly returns the same type as the first parameter. However, this typing fails when you reverse them:

AddTwo(Vector3.new(4, 5, 6), CFrame.new(1, 2, 3)) -- invalid

This is because adding them results in a CFrame, but we’re supposed to return the same type “T” as the first parameter, which is a Vector3 here.
I should also note that T and U can be the same type.

AddTwo(Vector3.new(1, 2, 3), Vector3.new(4, 5, 6)) -- valid

We can add another generic for the return type to fix the parameter ordering issue.

local function AddTwo<T, U, V>(a: T, b: U): V
    return a + b
end

AddTwo(Vector3.new(1, 2, 3), CFrame.new(4, 5, 6)) -- valid now

Now that literally everything is a separate generic, we’re coming pretty close to defeating the entire point of --!strict, but there are circumstances where you’d want it. Make the call as you see fit. The code you shared is a good example of using generics wisely.There is still plenty of structure to the “map” function to dictate what is allowed and what is not.


Now, for the code you shared.

-- `map` takes three generics named T, U, and V.
-- the "..." after "V" means that "V" can actually represent multiple types.
-- don't confuse this for "...V" like I did initially.
function map<T, U, V...>(

-- the first parameter is "callback"
-- callback must be a function that takes two parameters:
-- type T and zero or more arguments of any type,
-- so long as they match the "V..." parameters above.
-- it returns a U object.
        callback : (T, V...) -> (U),

-- the second parameter is "tbl"
-- tbl must be an Array of T objects
-- Array is not a built in type, but it's likely defined like so:
-- type Array<T> = {T}
        tbl : Array<T>,

-- the last parameter is "...", a variadic parameter.
-- it represents zero or more objects of any type,
-- so long as they match the V... types in the callback parameter.
        ... : V...

-- the function returns another function which takes no parameters and returns
-- either an object of type U, or nil.
) : () -> U?

Knowing all of this, here is a valid call to this function based on the information available.

-- I will choose to make type T "string" and type U "number".
-- I've learned that I can make type "V..." to be more specific,
-- so for an updated example I will use a string and a bool.

-- as defined in the "map" parameter: (T, V...) -> (U)
-- this function takes an input string and one or more letters
-- and returns how many letters in the input string match one of the letters.
-- "V..." is chosen to be the "target" and "blacklist" parameters.
local callback = function(object: string, target: string, blacklist: boolean): number
	local target_letters = string.split(target, "")
	local sum = 0
	for letter = 1, #object do
		if table.find(target_letters, object:sub(letter, letter)) then
			if not blacklist then
				sum += 1
			end
		elseif blacklist then
			sum += 1
		end
	end
	return sum
end

-- as defined in the "map" parameter: Array<T>
-- I'm using a table because, as mentioned, idk what an Array is defined as.
-- most likely it's a shortcut to say "any list of same-typed objects".
local tbl = {"firetruck", "racecar", "aeroplane", "submarine", "freight train"}

-- as defined in the "map" return type: () -> U?
-- you probably won't recognize this and it's not specified,
-- but based on context and the optional U return type,
-- it's likely that it's an iterator function to be used in a for loop.

-- the "V..." parameters as used in "callback" are a string and a boolean.
-- the string is what letters to look for, and the boolean specifies
-- either blacklist or whitelist.
local result_iterator = map(callback, tbl, "aeiou", false)

-- okay so result_iterator is a function that returns a U type,
-- which I've set to be a number according to the "callback" function I wrote.
-- my guess is that putting result_iterator in a for loop will loop over an
-- array of "U" or "number" objects.
-- this also makes sense because "map" is a common function
-- in some other languages which transforms a table into a different table,
-- either transforming things from one type to another, or performing operations
-- on a single type.
for count in result_iterator do
    -- this should print the number of vowels each word has in the table above.
    print(count)
    -- the output I expect:
    --> 3
    --> 3
    --> 5
    --> 4
    --> 4
end

Here’s a simpler example so you can more easily see how “map” style functions can be put to practical use.

-- using type T as Instance, type U as string, and type V as `any`.
-- we don't actually need type V but I believe it's required in the callback function.

-- get the full name of an instance. easy sauce.
-- "V..." represents any number of any types, and in this case
-- we don't want any additional types so we're ommitting it.
local get_full_name = function(object: Instance): string
    return object:GetFullName()
end

-- get all of the descendants of workspace.
local descendants = game.Workspace:GetDescendants()

-- get all of the full names of the descendants of workspace.
-- note: names is an iterator function, not a table. we can't do names[1].
local names = map(get_full_name, descendants)

-- the for loop calls "names" every loop to get a different value.
for name in names do
    print(name)
end

I’ve edited the code in between the breaks since I’ve learned the difference between V... and ...V.
...V is any number of objects of type V, and it doesn’t appear in this code.
V... is any number of any different types of objects, so long as the types represented by V... are consistent in every location that V... appears.

16 Likes

Wow, this is comprehensive, thank you! And yes, it is an iterator function.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.