feat: Support iter.Seqs in [Not]Contains and [Not]ElementsMatch#1685
feat: Support iter.Seqs in [Not]Contains and [Not]ElementsMatch#1685misberner wants to merge 1 commit intostretchr:masterfrom
iter.Seqs in [Not]Contains and [Not]ElementsMatch#1685Conversation
dolmen
left a comment
There was a problem hiding this comment.
We don't need to guard against Go 1.23. Iterator functions can be implemented in any version of Go, not just the ones with the iter package or the for loops syntactic sugar.
|
|
||
| yieldFunc := reflect.MakeFunc(paramType, func(args []reflect.Value) []reflect.Value { | ||
| result = reflect.Append(result, args[0]) | ||
| return []reflect.Value{reflect.ValueOf(true)} |
There was a problem hiding this comment.
Allocate []reflect.Value{reflect.ValueOf(true)} out of the yieldFunc to allow reuse.
|
|
||
| elemType := paramType.In(0) | ||
| resultType := reflect.SliceOf(elemType) | ||
| result := reflect.MakeSlice(resultType, 0, 0) |
There was a problem hiding this comment.
It is not necessary to allocate a zero element slice, as it will be discarded on the first call to append.
Instead a nil slice is just enough:
| result := reflect.MakeSlice(resultType, 0, 0) | |
| result := reflect.New(resultType).Elem() |
There was a problem hiding this comment.
Indeed not necessary, thanks for pointing this out. Though I'll go with this to save a new:
| result := reflect.MakeSlice(resultType, 0, 0) | |
| result := reflect.Zero(resultType) |
| }) | ||
|
|
||
| // Call the function with the yield function as the argument | ||
| xv.Call([]reflect.Value{yieldFunc}) |
There was a problem hiding this comment.
This extracts only the first element of the sequence. From the description of the function I expect the whole sequence to be serialized into the slice using a loop.
There was a problem hiding this comment.
This is incorrect, as otherwise obviously tests wouldn't be passing. The iteration itself happens in the function wrapped in xv. A for loop ranging over this function is syntactic sugar/magic for treating this single call as a coroutine, switching control between the function body and the loop head/body. If we call this function w/o the for/coroutine magic, it will just do a full iteration, calling yieldFunc for every element.
| @@ -0,0 +1,44 @@ | |||
| //go:build go1.23 || goexperiment.rangefunc | |||
There was a problem hiding this comment.
This compile guard doesn't seem necessary. There is nothing in seqToSlice implementation that requires Go 1.23. Sequences functions can be implemented in Go below 1.23. Go 1.23 only adds syntactic sugar in for loops.
There was a problem hiding this comment.
Fair. My thinking was that w/o range over func, there is no such thing as a "sequence" in Go and thus no basis for semantically treating a function of that form as a sequence (I am not aware of any use of the yield func pattern in Go outside of/before range-over-func). But I agree this is pedantry, so fine to change it in the name of simplification.
| @@ -0,0 +1,179 @@ | |||
| //go:build go1.23 || goexperiment.rangefunc | |||
There was a problem hiding this comment.
I don't see a reason for this guard (see my other comment in seq_supported.go).
|
|
|
Thanks for your review.
Could you please elaborate on why? Note that sequences aren't iterators and that sequences may be such that they can only be consumed once, but an However, I believe this is anyway not a realistic ask because of how sequences are implemented. Without range-over-func and the associated coroutine magic (see other comment), there just isn't a way to control the iteration from the caller side (you can abort it by returning LMK your thoughts. |
Sequences may also be almost infinite. For example, a sequence could provide even func EvenInc(start int64) func(yield func(int64) bool) {
if start%1 == 1 {
start++
}
return func(yield func(int64) bool) {
for {
if !yield(start) {
break
}
start += 2
}
}
}Full code: https://go.dev/play/p/19ADbf5mf8g Limiting the set of sequences usable with assert.Contains(t, EvenInc(0), int64(42), "42 is in the set of even numbers starting from zero") |
So we should either implement handling of sequences differently to not check for emptyness that early, or just not add support for sequences. |
Not saying an implementation should actively disallow it but I think there's a pretty obvious reason for not supporting it: because for the general case of infinite sequences, a assert.NotContains(t, EvenInc(0), int64(37), "37 is an odd number and thus not in the set of even numbers starting from zero")This obviously won't work, and fundamentally cannot be made to work. Hence it seems a very odd case to optimize for, especially if that optimization requires a major rework/limited reuse of existing code. |
|
It would be nice to have this. |
Originally from upstream: stretchr#1685 NOTE: did not add SeqElementsMatch as the origin PR suggested. Contains is good enough. Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
Summary
This PR adds support
iter.Seq[...]sequences (introduced in Go 1.23, or earlier viarangefuncexperiment) in theContainsandElementsMatch(+ derived) asserters.Changes
seqToSlicehelper that transparently converts aniter.Seq(which is a function of signaturefunc(func(T) bool)) to a corresponding slice of type[]T, if support for sequences is enabled. Otherwise, it does nothing. Note: this function is reflection-based and does not introduce any source dependencies on newer language features, thereby respecting the Go version constraint in thego.modfile.seqToSlicehelper in the above-mentioned asserters to contain any inputs from sequence to slice form (this has to happen early on because of the empty check)Motivation
Sequences are a new language feature that as of go1.23 has become standard for obtaining sequences of values w/o requiring to materialize the values into a slice (e.g.,
maps.Keys(...),maps.Values(...)). Currently, using these sequences in matchers requires materialization viaslices.Collect. However, sinceElementsMatchandContainsare conceptually applicable to sequences in addition to slices, arrays etc., this should be handled by the testify framework.Example usage
requires go1.23