Constrain a type to be either a slice or a map

382 views
Skip to first unread message

RussellLuo

unread,
Mar 18, 2022, 12:20:37 AM3/18/22
to golang-nuts
Hi there,

Thanks to Go generics in 1.18, I can write a generic function `LenBetween` for a slice:

```go
func SliceLenBetween[T ~[]E, E any](s T, min, max int) bool {
        return len(s) >= min && len(s) <= max
}
```

as well as for a map:

```go
func MapLenBetween[T map[K]V, K comparable, V any](s T, min, max int) bool {
        return len(s) >= min && len(s) <= max
}
```

Is there any way to write a constraint, say, SliceOrMap, to support either a slice or a map?

With the help of SliceOrMap, then I can write a more generic version `LenBetween` like this:

```go
func MapLenBetween[T SliceOrMap](s T, min, max int) bool {
        return len(s) >= min && len(s) <= max
}
```

Thanks in advance!

Henry

unread,
Mar 18, 2022, 3:07:17 AM3/18/22
to golang-nuts
Have you considered this?
```go
func IsBetween(value, min, max int) bool {
   return value>=min && value <=max
}

if IsBetween(len(myMap), 10, 25) {
  //do something
}
```

Dan Kortschak

unread,
Mar 18, 2022, 4:13:22 AM3/18/22
to golan...@googlegroups.com
On Thu, 2022-03-17 at 18:47 -0700, RussellLuo wrote:
> Is there any way to write a constraint, say, SliceOrMap, to support
> either a slice or a map?
>
> With the help of SliceOrMap, then I can write a more generic version
> `LenBetween` like this:
>
> ```go
> func MapLenBetween[T SliceOrMap](s T, min, max int) bool {
> return len(s) >= min && len(s) <= max
> }
> ```

Not yet AFAICS, but see https://github.com/golang/go/issues/51338. If
something like that is adopted, then you could write
https://go.dev/play/p/4RyDr_u1WAM.


RussellLuo

unread,
Mar 18, 2022, 5:06:32 AM3/18/22
to golang-nuts
Thanks, kortschak!

I still need some time to understand the proposal in https://github.com/golang/go/issues/51338, but I think the code provided in the Go Playground is exactly what I want!

RussellLuo

unread,
Mar 18, 2022, 6:47:29 AM3/18/22
to golang-nuts
Thanks for your reply, Henry!

I admit that my problem seems a little bit silly without providing the contextual information. Actually I am trying to rewrite my little [validation library](https://github.com/RussellLuo/validating) by leveraging Go generics.

For the originally non-generic [Len](https://pkg.go.dev/github.com/RussellLuo/validating/v2#Len) validator factory, I have implemented two generic versions. One for a slice:

```go
// LenSlice is a leaf validator factory used to create a validator, which will
// succeed when the length of the slice field is between min and max.
func LenSlice[T ~[]E, E any](min, max int) (mv *MessageValidator) {
        mv = &MessageValidator{
                Message: "with an invalid length",
                Validator: Func(func(field *Field) Errors {
                        v, ok := field.Value.(T)
                        if !ok {
                                return NewUnsupportedErrors(field, "LenSlice")
                        }

                        l := len(v)
                        if l < min && l > max {
                                return NewErrors(field.Name, ErrInvalid, mv.Message)
                        }
                        return nil
                }),
        }
        return
}
```

and the other for a map:

```go
// LenMap is a leaf validator factory used to create a validator, which will
// succeed when the length of the map field is between min and max.
func LenMap[T map[K]V, K comparable, V any](min, max int) (mv *MessageValidator) {
        mv = &MessageValidator{
                Message: "with an invalid length",
                Validator: Func(func(field *Field) Errors {
                        v, ok := field.Value.(T)
                        if !ok {
                                return NewUnsupportedErrors(field, "LenMap")
                        }

                        l := len(v)
                        if l < min && l > max {
                                return NewErrors(field.Name, ErrInvalid, mv.Message)
                        }
                        return nil
                }),
        }
        return
}
```

As a result, we can use them as below:

```go
Schema{
        F("slice", []string{"foo"}): LenSlice[[]string](0, 2),
        F("map", map[string]int{"a": 1, "b": 2}): LenMap[map[string]int](0, 2),
}
```

Now I am wondering if I can merge the above two versions into one generic `Len`, with the help of a constraint, say, SliceOrMap. For example:

```go
func Len[T SliceOrMap](min, max int) (mv *MessageValidator) {
        mv = &MessageValidator{
                Message: "with an invalid length",
                Validator: Func(func(field *Field) Errors {
                        v, ok := field.Value.(T)
                        if !ok {
                                return NewUnsupportedErrors(field, "Len")
                        }

                        l := len(v)
                        if l < min && l > max {
                                return NewErrors(field.Name, ErrInvalid, mv.Message)
                        }
                        return nil
                }),
        }
        return
}
```

Then we can use the same `Len` for both a slice and a map as below instead:

```go
Schema{
        F("slice", []string{"foo"}): Len[[]string](0, 2),
        F("map", map[string]int{"a": 1, "b": 2}): Len[map[string]int](0, 2),
}
```

(NOTE: I have posted this reply earlier, but is was not displayed (maybe due to my wrong choice of "Reply to author").  Now I post it again by selecting "Reply all".)

Henry

unread,
Mar 18, 2022, 11:53:20 PM3/18/22
to golang-nuts
I don't think there is such support in Go generics as of now. I may be wrong though. 

The alternative is to use reflection. Here is the simplified code of your example:

```go
func GetValidator(field Field) Validator {
        return Validator{
                Validate: func() (int, error) {
                        value := reflect.ValueOf(field.Value)
                        if value.Kind() != reflect.Map && value.Kind() != reflect.Slice && value.Kind() != reflect.Array {
                                return 0, errors.New("invalid type")
                        }
                        return value.Len(), nil
                },
        }
}
```

RussellLuo

unread,
Mar 19, 2022, 2:20:52 AM3/19/22
to golang-nuts
Thank you again! By using Go reflection, the code you provided is indeed simple and elegant.

Unfortunately, for better performance and less magic, the main motivation behind my validation library is to try to avoid using Go reflection. So you may have noticed that I have used lots of type assertions for the currently non-generic implementation.

With the help of Go generics, the implementation code certainly can be largely simplified and can handle much more non-primitive types properly. For now, I think providing both `LenSlice` and `LenMap` may be also good, although not as good as the single `Len`.
Reply all
Reply to author
Forward
0 new messages