Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constraint satisfying 3-array of constraints.Float with methods

Tags:

generics

go

One of the things I have been looking forward to with Go 1.18's generics is to use a 3-element vector type which can take float32 or float64 for its elements. Come today, this is now possible:

import "golang.org/x/exp/constraints"

type Vec3[T constraints.Float] [3]T

func (vec Vec3[T]) Add(other Vec3[T]) Vec3[T] {
    return Vec3[T]{vec[0] + other[0], vec[1] + other[1], vec[2] + other[2]}
}

func (vec Vec3[T]) Mul(factor T) Vec3[T] {
    return Vec3[T]{vec[0] * factor, vec[1] * factor, vec[2] * factor}
}

I then wonder, what would an according constraint look like? Omitting the methods, I arrive at the following solution:

type Vec3ish[T constraints.Float] interface {
    ~[3]T
}

A function taking according arguments could look like so:

func addVecs[A Vec3ish[T], T constraints.Float](a, b A) A {
    return A{a[0] + b[0], a[1] + b[1], a[2] + b[2]}
}

This seems verbose. Is it really necessary to define both T and A?

Things get worse when we demand Add and Mul to be implemented:

type Vec3ish[T constraints.Float] interface {
    ~[3]T
}

type Vec3ishUseful[T constraints.Float, A Vec3ish[T]] interface {
    Vec3ish[T]
    Add(A) A
    Mul(T) A
}

func addVecsGeneric[U Vec3ishUseful[T, A], A Vec3ish[T], T constraints.Float](a U, b A) A {
    return a.Add(b)
}

An extra type, three generic type parameters, and having to give a and b separate types. Going further, I see no way to get the expression a.Add(b).Add(b.Add(a)) to work within the function. This isn't the future I was hoping for. What needs to happen here?

like image 338
Zyl Avatar asked Mar 05 '26 17:03

Zyl


1 Answers

This seems verbose. Is it really necessary to define both T and A?

Yes, it is.

Your interface Vec3ish[T constraints.Float] is itself parametrized, so you must instantiate it when using it as a constraint. Given the type constraint A Vec3ish[T] you can see that a function using it is also parametrized in T, and one T that fulfils the constraint in Vec3ish at compile-time.

The function declaration might look somewhat verbose, but at the call sites it will not, since type inference will do all the legwork:

v1 := Vec3[float64]{1, 2, 3}
v2 := Vec3[float64]{5, 6, 7}
result := addVecs(v1, v2)

Slightly less verbose would be to use the shorthand for the vector constraint, but if you plan to reuse the Vec3ish constraint, the named interface might be better:

func addVecs[A ~[3]T, T constraints.Float](a, b A) A {
    return A{a[0] + b[0], a[1] + b[1], a[2] + b[2]}
}

Same goes for the other interface, since there you want to capture both T and A, but again, at call site, it gets better:

addVecsGeneric(v1, v2)

One might think to use the parametrized interface as type of the argument and let type inference do the rest:

type Vec3ishUseful[T constraints.Float, A Vec3ish[T]] interface {
    Add(A) A
    Mul(T) A
}

// Instantiating Vec3ishUseful with type params T and A 
func addVecsGeneric[A Vec3ish[T], T constraints.Float](a Vec3ishUseful[T, A], b A) A {
    return a.Add(b)
}

// addVecsGeneric(v1, v2) doesn't compile

However this does not work (yet?). There's a quite extensive discussion about this here. So by going this route, you have to let go of inference and pass the type parameters explicitly. This somewhat distributes verbosity between function signature and callers:

addVecsGeneric[Vec3[float64], float64](v1, v2)
like image 70
blackgreen Avatar answered Mar 07 '26 07:03

blackgreen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!