I recently found myself having to work with some 2d slices in Go, when it hit
me, go’s slices
package has no Flatten
or Map
functions. I mainly needed to extract
these slices from map
’s, sort them, filter and pass them to other iterator
based slice
functions (iter
and
slices
).
1var sets = map[string][]string{
2 "nato": {
3 "Alfa",
4 "Bravo",
5 // ...
6 "Zulu",
7 },
8 "german": {
9 "Anton",
10 "Berta",
11 // ...
12 "Zacharias",
13 },
14}
15
16func main() {
17 // phonetic alphabet stuff
18
19 // there are 26 chars in an alphabet and each set is an alphabet
20 flattend := make([]int, len(sets)*26)
21 idx := 0
22 for inner := range maps.Values(sets) {
23 for _, innerVal := range inner {
24 l := len(innerVal)
25 if l != 0 {
26 flattend[idx] = l
27 idx += 1
28 }
29 }
30 }
31 sum := 0
32 slices.Sort(flattend)
33 for _, v := range flattend {
34 sum += v
35 }
36 fmt.Println(sum)
37}
$ go run .
300
Where is Flatten (+Map) and Why even Flatten
Most, if not all, slice
functions accept
iter.Seq
:
1// Seq is an iterator over sequences of individual values.
2// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
3// stopping early if yield returns false.
4// See the [iter] package documentation for more details.
5type Seq[V any] func(yield func(V) bool)
The issue is: converting any 2d slice ([][]T
) to an iterator is generally
done via slices.All
, which yields
iter.Seq2
1// All returns an iterator over index-value pairs in the slice
2// in the usual order.
3func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
4 return func(yield func(int, E) bool) {
5 for i, v := range s {
6 if !yield(i, v) {
7 return
8 }
9 }
10 }
11}
1// Seq2 is an iterator over sequences of pairs of values, most commonly key-value pairs.
2// When called as seq(yield), seq calls yield(k, v) for each pair (k, v) in the sequence,
3// stopping early if yield returns false.
4// See the [iter] package documentation for more details.
5type Seq2[K, V any] func(yield func(K, V) bool)
For instance, passing [][]string
to slices.All
computes the following
generic:
1// func[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]
2func slices.All(s [][]string) iter.Seq2[int, []string]
And none of the below functions accept the above iter.Seq2
:
1func Collect[E any](seq iter.Seq[E]) []E
2func AppendSeq[Slice ~[]E, E any](s Slice, seq iter.Seq[E]) Slice
3func Sorted[E cmp.Ordered](seq iter.Seq[E]) []E
4func SortedFunc[E any](seq iter.Seq[E], cmp func(E, E) int) []E
5func SortedStableFunc[E any](seq iter.Seq[E], cmp func(E, E) int) []E
So I would need to consume each iterator value and consume all its elements before moving the resulting temporary container into yet another iterator to pass into all other, iterator consuming functions.
Rust has Iterator.flatten
and every other “functional like” language
constructs in not so functional languages (JavaScript, etc.) contain them too.
1// [5, 5]
2vec![vec!["hello"], vec!["world"]]
3 .iter()
4 .flatten()
5 .map(|word| word.len())
6 .collect::<Vec<_>>();
1[["hello"], ["world"]].flat().map(word => word.length);
So I’m asking: Where the fuck are slices.Flatten
and slices.Map
?:
1// []string{"hello", "world"}
2slices.Collect(
3 slices.Map(
4 slices.Flatten(
5 slices.All([][]string{{"hello"}, {"world"}}),
6 ),
7 func(in string) int {
8 return len(in)
9 },
10 ),
11)
On an whole other note: map.Values
from the example returns iter.Seq
while slices.All
returns iter.Seq2
, so we will need to write two
Flatten
implementations due to Go having a not really generic
generic system.
Also this sucks to look at, but I already wrote an article about that: Fun with Go Iterators.
A Flatten, a simpler Flatten and an even simpler Map
Answering the above question:
Even though Go iterators are really weird to write, but easy to consume, these are trivial to understand:
1// Map takes a Seq[T] and applies fn to each element, producing a Seq[V].
2func Map[T any, V any](seq iter.Seq[T], fn func(T) V) iter.Seq[V] {
3 return func(yield func(V) bool) {
4 seq(func(t T) bool {
5 v := fn(t)
6 return yield(v)
7 })
8 }
9}
10
11// Flatten flattens Seq[[]T] into Seq[T]
12func Flatten[T any](outer iter.Seq[[]T]) iter.Seq[T] {
13 return func(yield func(T) bool) {
14 outer(func(inner []T) bool {
15 for _, val := range inner {
16 if !yield(val) {
17 return false
18 }
19 }
20 return true
21 })
22 }
23}
24
25// FlattenSeq2 flattens Seq2[K, []T] into Seq[T]
26func FlattenSeq2[T any, K any](outer iter.Seq2[K, []T]) iter.Seq[T] {
27 return func(yield func(T) bool) {
28 outer(func(_ K, inner []T) bool {
29 for _, val := range inner {
30 if !yield(val) {
31 return false
32 }
33 }
34 return true
35 })
36 }
37}
Reworking the example we started out with with the new Flatten
and Map
iterator functions:
1func main() {
2 sum := 0
3 for _, v := range slices.Sorted(
4 Map(
5 Flatten(maps.Values(sets)),
6 func(in string) int {
7 return len(in)
8 },
9 ),
10 ) {
11 sum += v
12 }
13 fmt.Println(sum)
14}
And using Flatten
with slices.All
via
1FlattenSeq2(slices.All([][]string{}))
For example:
1// gen produces `amount` of tuples of len `tupleSize` with unique
2// members of sets
3func gen(sets [][]string, amount int, tupleSize int) [][]string {
4 allWords := slices.Collect(FlattenSeq2(slices.All(sets)))
5 allWordsLen := len(allWords)
6 r := make([][]string, amount)
7 for i := range amount {
8 allWordsLocal := make([]string, allWordsLen)
9 copy(allWordsLocal, allWords)
10 r[i] = make([]string, toupleSize)
11 for j := range toupleSize {
12 idx := rand.Intn(len(allWordsLocal) - 1)
13 r[i][j] = allWordsLocal[idx]
14 allWordsLocal[idx] = allWordsLocal[len(allWordsLocal)-1]
15 allWordsLocal = allWordsLocal[:len(allWordsLocal)-1]
16 }
17 }
18 return r
19}
Where are the other iterator helpers and why are Go iterators like this?
Go iterators are probably a good idea? I don’t think they fit into the language
at all and they lack so many features that I perceive them to be somewhat
unfinished. Especially compared to something like the interface system and how
most packages feel designed around the idea of plug and play Writer
and
Reader
interaction. Maybe I am too spoiled by rust and pre generic Go.
Since I’m already here, where are:
Filter
FlatMap
FilterMap
Sum
- …
Also, I don’t have (or want) a google account so I won’t contribute any
of these, nor Flatten
or Map
, to the Go project - even disregarding
the fact that they probably wouldn’t be accepted as part of slices
.