March 23, 2023

On Wednesday, 22 March 2023 at 12:30:46 UTC, FeepingCreature wrote:

>

You cannot constrain a type to be Vector3 in D

That's the problem.

>

you cannot in C++ either.

Yes, I can.

template <typename T>
using Vector3 = Matrix<T, 3, 1>;

template <typename T>
void foo(const Vector3<T>& v) {
}

int main() {
	foo(Vector3<float>());
}

I can declare parameters with Vector3<T>, and it gets me the right T, which is all I need.

D allows me to declare template alias parameter but it matches nothing at all. Then why allow people to write template alias parameters at all?

March 23, 2023

On Thursday, 23 March 2023 at 00:36:12 UTC, Elfstone wrote:

>

Yes, I can.

template <typename T>
using Vector3 = Matrix<T, 3, 1>;

template <typename T>
void foo(const Vector3<T>& v) {
}

int main() {
	foo(Vector3<float>());
}

I can declare parameters with Vector3<T>, and it gets me the right T, which is all I need.

D allows me to declare template alias parameter but it matches nothing at all. Then why allow people to write template alias parameters at all?

It is interesting that this

struct B (T, V) { }

alias A = B!(double, void);

void f (A) { }

void main ()
{
   A a;
   f (a);
   B!(double, void) b;
   f (b); // works
}

compiles while the version with incomplete specialization requires a version of f not using the alias template in the parameter list:

struct B (T, V) { }

alias A (T) = B!(T, void);

// void f (T) (A!(T)) { } // fail
void f (T) (B!(T, void)) { } // cannot use A!T here, why not?

void main ()
{
   A!double a;
   f (a); // does not call void f (T) (A!(T))
   B!(double, void) b;
   f (b); // does not call void f (T) (A!(T))
}

In the failing case dmd says

vm.d(11): Error: none of the overloads of template `vm.f` are callable using argument types `!()(B!(double, void))`
vm.d(5):        Candidate is: `f(T)(A!T)`
vm.d(13): Error: none of the overloads of template `vm.f` are callable using argument types `!()(B!(double, void))`
vm.d(5):        Candidate is: `f(T)(A!T)`

It seems that dmd views A!double and B!(double, void) as different types which they aren't. Isn't there an alias-expansion phase during compilation?

[1] https://issues.dlang.org/show_bug.cgi?id=23798

March 24, 2023

On Thursday, 23 March 2023 at 12:40:47 UTC, kdevel wrote:

>

On Thursday, 23 March 2023 at 00:36:12 UTC, Elfstone wrote:

>

Yes, I can.

template <typename T>
using Vector3 = Matrix<T, 3, 1>;

template <typename T>
void foo(const Vector3<T>& v) {
}

int main() {
	foo(Vector3<float>());
}

I can declare parameters with Vector3<T>, and it gets me the right T, which is all I need.

D allows me to declare template alias parameter but it matches nothing at all. Then why allow people to write template alias parameters at all?

It is interesting that this

struct B (T, V) { }

alias A = B!(double, void);

void f (A) { }

void main ()
{
   A a;
   f (a);
   B!(double, void) b;
   f (b); // works
}

compiles while the version with incomplete specialization requires a version of f not using the alias template in the parameter list:

struct B (T, V) { }

alias A (T) = B!(T, void);

// void f (T) (A!(T)) { } // fail
void f (T) (B!(T, void)) { } // cannot use A!T here, why not?

void main ()
{
   A!double a;
   f (a); // does not call void f (T) (A!(T))
   B!(double, void) b;
   f (b); // does not call void f (T) (A!(T))
}

In the failing case dmd says

vm.d(11): Error: none of the overloads of template `vm.f` are callable using argument types `!()(B!(double, void))`
vm.d(5):        Candidate is: `f(T)(A!T)`
vm.d(13): Error: none of the overloads of template `vm.f` are callable using argument types `!()(B!(double, void))`
vm.d(5):        Candidate is: `f(T)(A!T)`

It seems that dmd views A!double and B!(double, void) as different types which they aren't. Isn't there an alias-expansion phase during compilation?

[1] https://issues.dlang.org/show_bug.cgi?id=23798

You're misunderstanding the problem.

When you call a template function, DMD (and C++) performs IFTI, implicit function template instantiation. This requires reverse-engineering a type T that fulfills the requirement for the function call. For void f(T)(A!T), DMD has to unify the given parameter type with the abstract type expression A!T and figure out a matching T.

If A is a struct, it can do this because for a struct type, DMD knows which template instance it came from and can just look up the parameter type that was used to create it. It looks it up, to be clear, in the caller's type. But an alias is not itself a type! So in the case where A is an alias, C++ and DMD have to do the opposite and figure out that A - not the type A!T, because we don't know T yet, but the A from the syntax node A!T in the callee! - is an alias template with a trivial expansion, so that for the purpose of type inference only, the abstract expression A!T can be treated as equivalent to B!(T, void) - still without having any concrete T, just a syntax node T.

So in C++, it's just as if you wrote void f(T)(B!(T, void)) {}, because it substitutes A!T, sight unseen, in the called function's parameter list.

That's why this works:

template <typename T, int M, int N>
struct Matrix {
};

template <typename T>
using Vector3 = Matrix<T, 3, 1>;

/**Exactly equivalent to template<typename T>
void foo(Vector<T> vector) {}*/
template<typename T>
void foo(Matrix<T, 3, 1> vector) {}

int main() {
  foo(Vector3<int>());
}

D does not do this yet.

March 24, 2023

On Friday, 24 March 2023 at 09:30:30 UTC, FeepingCreature wrote:

>

You're misunderstanding the problem.

When you call a template function, DMD (and C++) performs IFTI, implicit function template instantiation. This requires reverse-engineering a type T that fulfills the requirement for the function call.

It's important to understand that IFTI does something completely different than normal type inference: it does backwards type inference. It's more similar to Hindley-Milner than D's normal "forward" type resolution.

For instance, if you write a template like this:

void foo(T)(void delegate(T) dg) { }
foo((int i) {});

Then the compiler infers T as int by recursing on the expression "void delegate(T) = void delegate(int)". It takes the explicit template parameters and calling parameter types and uses them as nodes in an algebraic system, that it can use to solve for the template parameter types.

Or if you write a template like this:

T foo(T)(T a, T b) { }
foo(2, 3.5);

Then the compiler starts with two algebraic expressions: int : T and double : T (: meaning "is convertible to"), which it solves by unifying int and double into double.

It is important to understand that T here is a free variable, not yet a type; the point is to assign it a type. So we cannot instantiate a template with T, because we can only instantiate templates with concrete types.

Finally, in our example:

alias A(T) = B!T;
void foo(T)(A!T) {}
foo(B!int);

The compiler starts with an expression B!int = A!T, and is trying to solve for T.

It looks at A!T, and it thinks: "hm, the other term could be a template instantiation of the template A with something." But B!T is a template instantiation of B, not A, so it fails to match.

If we insert the missing step, what the compiler should do is: "Ah, B is a 'trivial alias' (like using in C++), so I can syntactically substitute it with its content", and create the new expression B!int = B!T. Then it can resolve this by noting that the concrete type B!int is an instance of B with the type int, and thus gain the new expression T = int, which solves the algebraic system.

March 24, 2023

On Friday, 24 March 2023 at 09:44:28 UTC, FeepingCreature wrote:

>

If we insert the missing step, what the compiler should do is: "Ah, B is a 'trivial alias'

My apologies, correction: A is a 'trivial alias'. Sorry for the minipost, but this topic demands precision.

March 24, 2023

On Friday, 24 March 2023 at 09:44:28 UTC, FeepingCreature wrote:

>

If we insert the missing step, what the compiler should do is: "Ah, B is a 'trivial alias' (like using in C++), so I can syntactically substitute it with its content", and create the new expression B!int = B!T. Then it can resolve this by noting that the concrete type B!int is an instance of B with the type int, and thus gain the new expression T = int, which solves the algebraic system.

That alone will be a huge improvement.

There's no need to resolve all cases. DIP1023 should not try to do that, if it's ever picked up.

template <typename T>
using Identity = int;

template <typename T>
void bar(Identity<T> id) {

}

int main() {
	bar(Identity<int>());
}

C++ doesn't automatically resolve this either, and I bet no one will ask it to.

When you think about it: for now, D allows writing template alias paramters, but AFAIK they will match absolutely nothing! Nothing you pass will ever be accepted.

March 24, 2023

On Friday, 24 March 2023 at 09:47:59 UTC, FeepingCreature wrote:

>

On Friday, 24 March 2023 at 09:44:28 UTC, FeepingCreature wrote:

>

If we insert the missing step, what the compiler should do is: "Ah, B is a 'trivial alias'

My apologies, correction: A is a 'trivial alias'. Sorry for the minipost, but this topic demands precision.

Thanks for explaining what goes on under the hood. I would like to branch to something entirely different, namely the original code Elfstone drafted in the first post:

struct Matrix(S, size_t M, size_t N)
{
}

alias Vec3(S) = Matrix!(S, 3, 1);

What is conceptually wrong with this code? Can't we read everywhere [1] that matrices with only one column ‘are’ (column) vectors?

However, a matrix is a two-dimensional aggregate, its elements are refered to with two indices. By contrast vectors are one-dimensional. Of course it is elegant to define the dot product of a (row) vector and a (column) vector in terms of matrix multiplication [2] but the interesting part here is the the ‘identification’ of the result (dot product) with the element (1, 1) of the resulting 1×1 matrix. Sometimes this precision gets lost, e.g. [3].

Hence: A vector should not be defined as a matrix with only column (or row).

[1] https://www.google.de/search?q=vectors+are+matrices+with+one+column
[2] https://en.wikipedia.org/wiki/Dot_product
[3] https://yutsumura.com/a-relation-between-the-dot-product-and-the-trace/

  • "Recall that v^Tw is, by definition, the dot product of the vectors v and w."
March 24, 2023

On Friday, 24 March 2023 at 09:44:28 UTC, FeepingCreature wrote:

>

[snip]

It's important to understand that IFTI does something completely different than normal type inference: it does backwards type inference. It's more similar to [Hindley-Milner] than D's normal "forward" type resolution.

I agree that it needs some kind of backwards type resolution.

Using the same initial example of the OP, consider something like below (that's not valid D code currently).

template Vec3(T) {
    alias Vec3 = Matrix!(T, 3, 1);

    template opResolveAlias(alias U)
    {
        static if(is(U == Matrix!(V, 3, 1), V))
            alias opResolveAlias = Vec3!V;
        else
            static assert(0);
    }
}

The Vec3 function has another template alias associated with it that handles the resolution, call it opResolveAlias. It can take some type U, check if it matches the right hand side of the original alias and then tells the compiler that it is a Vec3!V. For simple aliases, the opResolveAlias could be auto-generated by the compiler if not provided by the user. Regardless, the important thing would be that Vec3!V is kept as Vec3!V and not rewritten to Matrix!(V, 3, 1). You could even give it another notation, like this!V to make that effect clearer. Then, when the compiler is doing template alias resolution, it can tell that it is a template alias and do a re-write.

So for instance, if you have is(T == Vec3!S, S) then you can re-write it is(Vec3.opResolveAlias!T == Vec3!S, S). Functions might be a little more complicated, but the idea is the same: if you pass some T t into a function foo(S)(Vec3!S x) {}, then the resolution can similar check that Vec3!S is a template alias and apply Vec3.opResolveAlias!T to any incoming Ts.

March 24, 2023

On Friday, 24 March 2023 at 12:29:02 UTC, kdevel wrote:

>

[snip]

Thanks for explaining what goes on under the hood. I would like to branch to something entirely different, namely the original code Elfstone drafted in the first post:

struct Matrix(S, size_t M, size_t N)
{
}

alias Vec3(S) = Matrix!(S, 3, 1);

What is conceptually wrong with this code? Can't we read everywhere [1] that matrices with only one column ‘are’ (column) vectors?

[snip]

There's a lot that can get hidden in Matrix since this uses template parameters to define the dimensions of a Matrix (like a static array). For instance, it could specialize code for the case where N=1 so that it really behaves like a vector. It could have a special dot product function that behaves differently in this case vs. normal matrix multiplication (and could call that dot product code when doing a matrix multiplication instead, BLAS has ddot and dgemm and D is powerful enough to call different ones at compile time).

March 25, 2023

On Friday, 24 March 2023 at 12:29:02 UTC, kdevel wrote:

>

On Friday, 24 March 2023 at 09:47:59 UTC, FeepingCreature wrote:

>

On Friday, 24 March 2023 at 09:44:28 UTC, FeepingCreature wrote:

>

If we insert the missing step, what the compiler should do is: "Ah, B is a 'trivial alias'

My apologies, correction: A is a 'trivial alias'. Sorry for the minipost, but this topic demands precision.

Thanks for explaining what goes on under the hood. I would like to branch to something entirely different, namely the original code Elfstone drafted in the first post:

struct Matrix(S, size_t M, size_t N)
{
}

alias Vec3(S) = Matrix!(S, 3, 1);

What is conceptually wrong with this code? Can't we read everywhere [1] that matrices with only one column ‘are’ (column) vectors?

However, a matrix is a two-dimensional aggregate, its elements are refered to with two indices. By contrast vectors are one-dimensional. Of course it is elegant to define the dot product of a (row) vector and a (column) vector in terms of matrix multiplication [2] but the interesting part here is the the ‘identification’ of the result (dot product) with the element (1, 1) of the resulting 1×1 matrix. Sometimes this precision gets lost, e.g. [3].

Hence: A vector should not be defined as a matrix with only column (or row).

[1] https://www.google.de/search?q=vectors+are+matrices+with+one+column
[2] https://en.wikipedia.org/wiki/Dot_product
[3] https://yutsumura.com/a-relation-between-the-dot-product-and-the-trace/

  • "Recall that v^Tw is, by definition, the dot product of the vectors v and w."

The design can't be perfect for everything but it's good enough for many things. Eigen in C++ does the same and I think it's still one of the best libraries out there.
Actually the main benifit I see is that I don't have to specialize heavy operations such as Matrix * Vector with largely the same code as the general Matrix * Matrix. But I do happily define opIndex(size_t i) when M == 1 || N == 1, which is trivial.

It's but a convenient example to demonstrate the problem with D's alias anyway.