How to write a Go type constraint for something you can take len() of?

Issue

I am trying to write a type constraint for a Go program, that accepts "anything you can take len() of". But I can’t really figure it out.

I want something like:

LenOf[m Measurable](m M) int {
    return len(m)
}

I tried a few things. Fx. this naiive thing, which do compile, but doesn’t work on all types (like fx []User):

type Measurable interface {
    ~string | []any | ~map[any]any
}

Then went on to something like, the below which not only makes the function signature for LenOf() extremely clunky, but also have clumsy to write on call sites (and still can’t get it to compile)

type Measurable[K comparable, V any] interface {
   ~string | []V | ~map[K]V
}

Solution

Why? The builtin len is already "generic".


With that said, let’s see why defining such a constraint is a bad idea. The Go spec has a paragraph — Length and capacity, that can help:

If the argument type is a type parameter P, the call len(e) (or cap(e) respectively) must be valid for each type in P’s type set. The result is the length (or capacity, respectively) of the argument whose type corresponds to the type argument with which P was instantiated.

The issues with writing an all-encompassing constraint for "measurable" types are:

  • it includes arrays [N]T, where the array length is part of the type, so your constraint would have to specify all possible arrays you want to capture
  • it includes array pointers *[N]T, which you can’t easily abstract in a type constraint
  • it includes maps, which forces you to capture keys K and values V, which may or may not be the same as T. Plus, K must implement comparable.

So you’d have to write something like:

type Measurable[T any, K comparable, V any] interface {
    ~string | ~[]T | ~map[K]V | ~chan T
}

which notably doesn’t include arrays, and doesn’t distinctly capture pointer literals, e.g. to match []*int, you would have to instantiate T with *int.

You might simplify V away:

type Measurable[T any, K comparable] interface {
    ~string | ~[]T | ~map[K]T | ~chan T
}

The function LenOf then becomes:

func LenOf[T any, K comparable, M Measurable[T, K]](m M) int {
    return len(m)
}

but you still have to supply K, so you have to instantiate LenOf at call site with bogus types for the map key:

LenOf[string, int]("foo")
//            ^ actually useless

and you can’t take advantage of type inference with the argument either.

In conclusion: just use len. Design your generic functions to work with type literals that support length, or add to the constraint only those types that your functions are reasonably expected to handle.

Answered By – blackgreen

Answer Checked By – Candace Johnson (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.