Constrained variadic type parameters

I think it’s tricky to explain what I mean using plain old English, so I will try my best to explain it in Lua.

--!strict
local function variadic_type_parameters<T...>(...: T...): T...
    -- ...
end

local a: string, b: number, c: string = variadic_type_parameters("foo", 42, "bar")
--     ^-------   ^-------   ^------- These are all automatically inferred by the compiler!

type Wrap<T> = {
    inner: T
}

local function wrap<T>(inner: T): Wrap<T>
    -- ...
end

local function constrained_variadic_type_parameters<T...>(...: Wrap<T...>): T...
    -- ...                                                     ^--------- This is invalid syntax.
end

local a: unknown, b: unknown, c: unknown = constrained_variadic_type_parameters(wrap("foo"), wrap(42), wrap("bar"))
--     ^--------   ^--------   ^-------- I haven't found a way to make the compiler infer these.

Is there a way to make constrained_variadic_type_parameters work like variadic_type_parameters?

As far as I know, this is the closest you can get:

local function type_parameters<A, B, C>(a: A?, b: B?, c: C?): (A, B, C)
    -- ...                    ^-------- You can imagine a bunch of these.
end

local a: string, b: number, c: string = type_parameters("foo", 42, "bar")
--     ^-------   ^-------   ^------- These are all automatically inferred by the compiler!

local function constrained_type_parameters<A, B, C>(a: Wrap<A>?, b: Wrap<B>?, c: Wrap<C>?): (A, B, C)
    -- ...                                ^-------- You can imagine a bunch of these.
end

local a: string, b: number, c: string = constrained_type_parameters(wrap("foo"), wrap(42), wrap("bar"))
--     ^-------   ^-------   ^------- These are all automatically inferred by the compiler!

But that is problematic because it breaks if we go over, in this case, three arguments.

Do you mean variadic functions?

Yes, that’s exactly what I mean, but I’m talking explictly about type parameters, not actual function parameters.

Not exactly sure, what would be the difference? Can you give a simple example?

function identity<T>(value: T): T
    -- ...       ^-1 ^---2
    -- 1: type parameter
    -- 2: function parameter
end

To clarify, my question isn’t:
“How to make constrained_type_parameters return at runtime the values I want”,

It’s actually:
“How to make constrained_type_parameters return the types I want, with no runtime overhead”

Hm, still not 100% sure. Do you mean for example function identityString(only string variables)? If yes, you could just loop through all variables and break the function if any variable is not a string. If no, then I may not be helpful here :sweat_smile:

That would be a runtime check, you’d (at runtime) check if the variable is a string. I’m talking about typechecking, and generic type parameters. See the Luau documentation on typechecking for more information

Ahh, I think I gotcha now! Check this function typing out, add the code and add a “–!strict” on top of your script, it will now throw a warning when you use the function with a type different than allowed. That’s actually really good to know!

Yeah, exactly. The problem starts when you throw generics and variadics into the mix.

For example, let’s imagine you wanted a function that takes two input functions and returns a new function that returns the result of the two input functions:

local function join_fns(fn_1, fn_2)
    return function()
        return fn_1(), fn_2()
    end
end

It’s simple, and you can describe it in types:

function join_fns<A, B>(fn_1: () -> A, fn_2: () -> B): () -> (A, B)

But what if you wanted to make this work (at the type level) for an arbitrary number of functions?
That’s my problem right now.

Of course, I’m not trying to make join_fns a thing, but to solve my specific problem, I need to figure out how one would do that.

No it isn’t a stupid question, the problem is when you have a wrapper type for T instead of just T:

function fn_no_wrap<T...>(...: T...): T...
-- This is fine ^

function fn_wrap<T...>(...: Wrap<T...>): T...
-- This is the problem ^    ^--------- Haven't found a way to make this
1 Like

I’m not sure how your constrained_variadic_type_parameters is even valid code. Wrap only takes a single type, yet you’re passing a variadic template to it.

In a smarter language, we would’ve used

function constrained_variadic_type_parameters<T...>(...: Wrap<T>...): T...

although, I’m not sure if this is valid in any language at all.

I suppose some may claim that this is an XY problem, so I’ll explain what exactly I’m trying to achieve:

export type Tag<T> = {
    __fgsig: "Tag"
}

type Iterator<T...> = {
    next: (self: Iterator<T...>) -> T...
    -- ...
}

--- Queries for a single tag.
---
--- # Arguments
--- * `tag` - The tag to query for.
---
--- # Returns
--- An iterator over every instance of that tag.
local function query_single<T>(tag: Tag<T>): Iterator<T>
     -- ... This is done & good!
end

--- Queries for multiple tags.
--- 
--- # Arguments
--- * `...` - The tags to query for.
---
--- # Returns
--- An iterator over every instance with those tags.
local function query_iter<T...>(tag: Tag<T...>): Iterator<T...>
   -- This is done, but the type system isn't that good...
end

Yeah, it isn’t valid code. I was just trying to illustrate the concept.

// Some C++20 code
template <class T>
class Wrapper {};

template <typename ...T>
std::tuple<T...> foo(Wrapper<T>... wrap) {}

Maybe if Luau had a smarter type-checker, we’d be able to implement this. Specifically, the feature that is missing is the Wrapper<T>... part. This ellipsis syntax does a compile-time expansion and substitution for every T. It allows even wrapper classes to be expanded. It will also work in the body of the function, like so:

// Example class
template <class T>
class Wrapper {
private: 
    T m_inner;

public:
    Wrapper(T inner): m_inner(inner) {}

    T inner() {
        return this->m_inner;
    }
};

template <typename ...T>
std::tuple<T...> get_inner_of_many(Wrapper<T>... wrap) {
    return std::make_tuple(wrap.inner()...); // inner is run for every Wrapper<T>
}

For now, though, do as OP suggested.

1 Like

That’s sad. I guess I can always make a pull request later.

1 Like

For anybody here who wants a solution and doesn’t care about disgusting and ugly hacks, there are two options I found.

For the first one, go insane and add a bunch of type parameters:

local function constrained_type_parameters<A, B, C>(a: Wrap<A>?, b: Wrap<B>?, c: Wrap<C>?): (A, B, C)
    -- ...                                ^-------- You can imagine a bunch of these.
end

For the second one, LIE to the compiler:

type Wrap<T> = {
    inner: T,
    self: T -- actually Wrap<T>
}

local function constrained_type_parameters<T...>(...: T...): T...
    -- ...
end

constrained_type_parameters(wrap("Hello, world!").self)

This first one has a compile-time performance penalty, and the second one has a run-time performance penalty. Choose your hell.

Oh ok, then I still didn’t and don’t get it, maybe one day :joy:

1 Like

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