How to resolve unresolved parameter?

Consider I have two or more Tuples in a struct.

from std.reflection import get_type_name

def work_with_tuple[*elems: Movable](obj: Tuple[*elems]):
    print(get_type_name[type_of(obj)]())

struct Container[
    Tuple1: Copyable,
    Tuple2: Copyable,
](ImplicitlyCopyable):
    def __init__(out self, v1: Self.Tuple1, v2: Self.Tuple2):
        work_with_tuple(v1)

def main():
    Container((1,2), (3,4,5))

This, however creates a

tmp.mojo:11:9: error: invalid call to 'work_with_tuple': value passed to 'obj' cannot be converted from 'Tuple1' to 'Tuple[elems]', it depends on an unresolved parameter 'elems'
        work_with_tuple(v1)
        ^~~~~~~~~~~~~~~ ~~
/home/cloud/tmp.mojo:3:5: note: function declared here
def work_with_tuple[*elems: Movable](obj: Tuple[*elems]):

You can have something like the following

struct Container[
    # Tuple1: Copyable,
    Tuple2: Copyable,
](ImplicitlyCopyable):
    def __init__[*elems: Movable](out self, v1: Tuple[*elems], v2: Self.Tuple2):
        work_with_tuple(v1)
        # work_with_tuple(v2)

Then v1 can work_with_tuple, but v2 still not. You cannot have Tuple1 type now as it is dis-associated with the ctor, but you can comptime Tuple1 = Tuple[*Self.elems] by moving the elems to struct parameter.

But how to deal with v2 then? You cannot have two ‘*’ markers in the same parameter list.

In C++, the following is easily achievable. Compiler Explorer

#include <cstdio>
#include <tuple>

template <typename... Ts>
void work_with_tuple(std::tuple<Ts...>&& obj) {
    printf("work_with_tuple");
}

template <typename Tuple1_, typename Tuple2_>
struct Container {
    using Tuple1 = Tuple1_;
    using Tuple2 = Tuple2_;
    Container(Tuple1_&& v1, Tuple2_&& v2) {
        work_with_tuple(std::forward<Tuple1>(v1));
    }
};

int main() {
    Container(std::tuple(1,2), std::tuple(3,4,5));
}

as C++ template is unchecked.

Template parameters for Tuple1 and Tuple2 can be retrieved by other metaprogramming tricks then. I am wondering how to achieve these type of metaprogramming in mojo.

The reason this doesn’t work in Mojo is due to how the Mojo compilation model differs from the C++ one.
In C++ you can write

    Container(Tuple1_&& v1, Tuple2_&& v2) {
        work_with_tuple(std::forward<Tuple1>(v1));
    }

because c++ doesn’t do any type checking of templated types/functions until that template is actually instantiated. Since you could in C++ do

int main() {
    Container(1, 2);
}

And the error wouldn’t manifest until the compiler sees that work_with_tuple expects a tuple

In instantiation of 'Container<Tuple1_, Tuple2_>::Container(Tuple1_&&, Tuple2_&&) [with Tuple1_ = int; Tuple2_ = int]':
<source>:19:19:   required from here
   19 |     Container(1, 2);
      |                   ^
<source>:14:24: error: no matching function for call to 'work_with_tuple(int)'
   14 |         work_with_tuple(std::forward<Tuple1>(v1));
      |         ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:14:24: note: there is 1 candidate
<source>:5:6: note: candidate 1: 'template<class ... Ts> void work_with_tuple(std::tuple<_Elements ...>&&)'
    5 | void work_with_tuple(std::tuple<Ts...>&& obj) {
      |      ^~~~~~~~~~~~~~~
<source>:5:6: note: template argument deduction/substitution failed:
<source>:14:24: note:   mismatched types 'std::tuple<_Elements ...>' and 'int'
   14 |         work_with_tuple(std::forward<Tuple1>(v1));
      |         ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~

However, in Mojo the compiler needs proof that the values passed to work_with_tuple are actually tuples and not just anytype.
Which is why

struct Container[
    Tuple1: Copyable,
    Tuple2: Copyable,
](ImplicitlyCopyable):
    def __init__(out self, v1: Self.Tuple1, v2: Self.Tuple2):
        work_with_tuple(v1)

does not work. v1 and v1 could be any Copyable type, not just a Tuple. We need to prove to the compiler that v1 and v2 are actually tuples. The way you can write this is something as such:

from std.reflection import get_type_name

def work_with_tuple[*elems: Movable](obj: Tuple[*elems]):
    print(get_type_name[type_of(obj)]())

struct Container(ImplicitlyCopyable):
    def __init__(out self, v1: Tuple[...], v2: Tuple[...]):
        work_with_tuple(v1)
        work_with_tuple(v2)

def main():
    Container((1,2), (3,4,5))

Here we proving to the compiler that v1 and v2 are actually tuples. So it knows you can call work_with_tuple and never get a type error.

I think it’s important to note here that this behavior difference from C++ is one of the reasons that Mojo compiles so much faster. Local reasoning is a super important policy for performance.

This doesn’t work. Things are passed to ctor with reasons.

struct Container(ImplicitlyCopyable):
    var v1: ???
    var v2: ???????????????
    def __init__(out self, v1: Tuple[...], v2: Tuple[...]):
        work_with_tuple(v1)
        work_with_tuple(v2)

After playing around with this problem a little bit more during C++ compiling :sweat_smile: , I made some progress:

from std.reflection import get_type_name

def work_with_tuple[*elems: Movable](obj: Tuple[*elems]):
    print(get_type_name[type_of(obj)]())

struct Container[
    T1: type_of(Tuple[...]),  # So type_of(Tuple[...]) is a Trait for TupleLike?
    T2: type_of(Tuple[...]),
]:
    # Since T1 and T2 are TupleLike, they have .element_types, good!
    comptime Elems1 = Self.T1.element_types
    comptime Elems2 = Self.T2.element_types

    # Since T1 and T2 is known now, so we can store v1 and v2, good!
    var v1: Self.T1
    var v2: Self.T2

    def __init__(out self, var v1: Self.T1, var v2: Self.T2):
        work_with_tuple(v1)
        work_with_tuple(v2)
        # we need to move it unfortunately, Copyable is not inferred correctly.
        self.v1 = v1^
        self.v2 = v2^

def main():
    var v1 = (1,2)
    var v2 = (3,4,5)
    Container[type_of(v1), type_of(v2)](v1, v2)
    # Container(v1, v2)  # this does not compile, bad!

The last line failed, if uncommented, with errror as follows,

<source>:29:14: error: invalid initialization: failed to infer parameter 'T1' of parent struct 'Container'
    Container(v1, v2)  # this does not compile, bad!
    ~~~~~~~~~^~~~~~~~
<source>:6:8: note:  struct declared here
struct Container[
       ^
<source>:18:9: note: function declared here
    def __init__(out self, var v1: Self.T1, var v2: Self.T2):

@owenhilyard @nate any idea?

OK, more progress during more C++ compilation :sweat_smile:

from std.reflection import get_type_name

def work_with_tuple[*elems: Movable](obj: Tuple[*elems]):
    print(get_type_name[type_of(obj)]())

struct Container[
    T1: type_of(Tuple[...]),  # So type_of(Tuple[...]) is a Trait for TupleLike?
    T2: type_of(Tuple[...]),
]:
    # Since T1 and T2 are TupleLike, they have .element_types, good!
    comptime Elems1 = Self.T1.element_types
    comptime Elems2 = Self.T2.element_types

    # Since T1 and T2 is known now, so we can store v1 and v2, good!
    var v1: Self.T1
    var v2: Self.T2

    def __init__(out self, var v1: Self.T1, var v2: Self.T2):
        work_with_tuple(v1)
        work_with_tuple(v2)
        # we need to move it unfortunately, Copyable is not inferred correctly.
        self.v1 = v1^
        self.v2 = v2^

def detour(var v1: Tuple[...], var v2: Tuple[...]) -> Container[type_of(v1), type_of(v2)]:
    return Container[type_of(v1), type_of(v2)](v1^, v2^)

def main():
    _ = detour((1,2), (3,4,5))  # It worked! Ugly!

Is there any chance we can omit the detour to enforce the type inference?

This is similar to Comptime parameterized return type - #12 by jbemmel

The ‘rebind’ would ideally be inferred

Try

from std.reflection import get_type_name


def work_with_tuple[*elems: Movable](obj: Tuple[*elems]):
    print(get_type_name[type_of(obj)]())


struct Container[
    Elems1: Variadic.TypesOfTrait[Movable],
    Elems2: Variadic.TypesOfTrait[Movable],
]:
    # Since T1 and T2 is known now, so we can store v1 and v2, good!
    var v1: Tuple[*Self.Elems1]
    var v2: Tuple[*Self.Elems2]

    def __init__(
        out self, var v1: Tuple[*Self.Elems1], var v2: Tuple[*Self.Elems2]
    ):
        work_with_tuple(v1)
        work_with_tuple(v2)
        # we need to move it unfortunately, Copyable is not inferred correctly.
        self.v1 = v1^
        self.v2 = v2^


def main():
    _ = Container((1, 2), (3, 4, 5))

BTW, I think Chris is working on improving variadics as well, this will be improved soon.