Slicing Operations
Functions like Map/Reduce/Filter are often used in functional programming. Because of generics, we can also write Map/Reduce/Filter functions that can be adapted to any type 😂.
All three of these functions are used to process sliced data, so Go officially wanted to provide a standard slices package. But it’s subject to Rob’s objection, so it has to go under exp package underneath.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// The slices package implements a series of slicing algorithms.
package slices
// Map uses the map function to convert []T1 to []T2.
// This function has two type parameters, T1 and T2.
// The map function f accepts two type types, T1 and T2.
// This function can handle all types of sliced data.
func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
r := make([]T2, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
// Reduce uses the summary function to aggregate []T1 slices into a single result.
r := initializer
for _, v := range s {
r = f(r, v)
}
return r
}
// Filter uses the filter function to filter the data in a slice.
// The function returns a new slice, keeping only the elements for which the call to f returns true.
func Filter[T any](s []T, f func(T) bool) []T {
var r []T
for _, v := range s {
if f(v) {
r = append(r, v)
}
}
return r
}
|
Examples of use are as follows, with all type parameters determined by derivation.
1
2
3
4
5
6
7
8
9
10
|
s := []int{1, 2, 3}
floats := slices.Map(s, func(i int) float64 { return float64(i) })
// The values of floats are []float64{1.0, 2.0, 3.0}.
sum := slices.Reduce(s, 0, func(i, j int) int { return i + j })
// The value of sum is 6.
evens := slices.Filter(s, func(i int) bool { return i%2 == 0 })
// The value of evens is []int{2}.
|
Dictionary Operations
The following functions are given to extract a list of arbitrary dictionary keys.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// The maps package provides working dictionary handling functions.
package maps
// Keys returns a slice of the key of any dictionary.
// The order of the keys is not determined.
// This function takes two type parameters, K and V.
// Since the keys of a dictionary must support equal comparisons, K needs to satisfy the comparable constraint.
// The value of the dictionary can be of any type.
func Keys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
|
A typical example of use is as follows, with the type parameter determined by derivation.
1
2
|
k := maps.Keys(map[int]int{1:2, 2:4})
// Now the values of k are []int{1, 2} or []int{2, 1}.
|
Set Operations
Go’s map itself supports different types of K-V, so you can generally implement collection operations with map. However, with generics, we can also develop specialized collection types.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// sets package implements set functionality, elements need to support equality comparisons.
package sets
// Set is a collection of elements.
type Set[T comparable] map[T]struct{}
// Make constructs a collection object of a certain type.
func Make[T comparable]() Set[T] {
return make(Set[T])
}
// Add Adds v to the collection.
func (s Set[T]) Add(v T) {
s[v] = struct{}{}
}
// Delete Removes v from the collection.
func (s Set[T]) Delete(v T) {
delete(s, v)
}
// Contains the query if the collection contains v.
func (s Set[T]) Contains(v T) bool {
_, ok := s[v]
return ok
}
// Len returns the number of collection elements.
func (s Set[T]) Len() int {
return len(s)
}
// Iterate iterates through each element of the collection, executing the function f.
// The Delete method can be called in function f.
func (s Set[T]) Iterate(f func(T)) {
for v := range s {
f(v)
}
}
|
Examples of use are as follows.
1
2
3
4
5
6
7
8
9
10
|
// Create a new set of integers.
// Since the Make function does not use the type parameter T in its arguments, it is not possible to determine the actual type of T by type derivation
// The actual type of T cannot be determined by type derivation, but can only be specified manually.
s := sets.Make[int]()
// Add 1 to the set
s.Add(1)
// Make sure the set s does not contain the number 2
if s.Contains(2) { panic("unexpected 2") }
|
In general, however, a new collection defined using a generic type is not much different from using a map directly.
Sorting Operations
Because there are no generics, the Go language’s sort package provides three sort functions, Float64/Ints/Strings. If you want to sort floating point numbers or integer slices, you have to switch to float64/int slices first, which is very annoying. With the generic type, we can handle sorting algorithms of primitive types in a uniform way.
The elements of slices that support sorting need to support the compare size operation. For developers’ convenience, Go officially provides the constraints.Ordered
constraint to represent all built-in types that support sorting. So our unified algorithm can be written as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// orderedSlice implements the sort.Interface interface.
// The Less method uses the < operator. constraints.Ordered constraint ensures that type T supports the < operator.
type orderedSlice[T constraints.Ordered] []T
func (s orderedSlice[T]) Len() int { return len(s) }
func (s orderedSlice[T]) Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice[T]) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// OrderedSlice sorts the slices s in ascending order.
// The elements of s need to support the < operator.
func OrderedSlice[T constraints.Ordered](s []T) {
// Force s into orderedSlice[T], so that sorting can be done using sort.
sort.Sort(orderedSlice[T](s))
}
|
Examples of use are as follows.
1
2
3
4
5
6
7
|
s1 := []int32{3, 5, 2}
sort.OrderedSlice(s1)
// Now the value of s1 is []int32{2, 3, 5}
s2 := []string{"a", "c", "b"})
sort.OrderedSlice(s2)
// Now the value of s2 is []string{"a", "b", "c"}
|
The sort package also provides a Slice method, but it requires a reference to the slices being sorted via a closure, which is less convenient.
1
2
3
4
5
6
7
8
9
10
11
|
type Person struct {
Name string
Age int
}
people := []Person{
{"Gopher", 7},
{"Alice", 55},
{"Vera", 24},
{"Bob", 75},
}
sort.Slice(people, func(i, j int) bool { return people[i].Name < people[j].Name })
|
We can write a unified treatment using the generic type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// sliceFn implements the sort.Interface interface.
// The Less method actually calls the comparison function saved by cmp.
type sliceFn[T any] struct {
s []T
cmp func(T, T) bool
}
func (s sliceFn[T]) Len() int { return len(s.s) }
func (s sliceFn[T]) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) }
func (s sliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] }
// SliceFn sorts slice s according to the comparison function cmp.
func SliceFn[T any](s []T, cmp func(T, T) bool) {
sort.Sort(sliceFn[T]{s, cmp})
}
|
So the original comparison function could then be rewritten as follows.
1
|
sort.SliceFn(s, func(p1, p2 Person) bool { return p1.Name < p2.Name })
|
Channel operations
With generic types, we can unify some common channel operations. For example, the following example.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
// The chans package implements some channel manipulation algorithms.
package chans
import "runtime"
// Drain discards all elements of the chan.
func Drain[T any](c <-chan T) {
for range c {
}
}
// Merge merges two chans and outputs the elements to the other chan.
func Merge[T any](c1, c2 <-chan T) <-chan T {
r := make(chan T)
go func(c1, c2 <-chan T, r chan<- T) {
defer close(r)
for c1 != nil || c2 != nil {
select {
case v1, ok := <-c1:
if ok {
r <- v1
} else {
c1 = nil
}
case v2, ok := <-c2:
if ok {
r <- v2
} else {
c2 = nil
}
}
}
}(c1, c2, r)
return r
}
// Ranger provides a mechanism to notify the sender to quit when the receiver is no longer working.
//
// Ranger returns a Sender/Receiver pair. receiver provides a Next method to receive content.
// Sender provides a Send to send content and a Close method to stop sending.
// Next detects if the Sender is closed, and the Send method detects if the Receiver has exited.
func Ranger[T any]() (*Sender[T], *Receiver[T]) {
c := make(chan T)
d := make(chan bool)
s := &Sender[T]{values: c, done: d}
r := &Receiver[T]{values: c, done: d}
// If the receiver has been recalled, the sender will be notified using the finalizer.
runtime.SetFinalizer(r, r.finalize)
return s, r
}
// Sender is used to send messages to the Receiver.
type Sender[T any] struct {
values chan<- T
done <-chan bool
}
// Send sends a message to the receiver. Return false for failure to send.
func (s *Sender[T]) Send(v T) bool {
select {
case s.values <- v:
return true
case <-s.done:
// The recipient has exited
return false
}
}
// Close informs the receiver that the sending of the message has ended.
// The Sender object should not be used after the Close call.
func (s *Sender[T]) Close() {
close(s.values)
}
// Receiver is used to receive messages from Sender.
type Receiver[T any] struct {
values <-chan T
done chan<- bool
}
// Next returns the received message. If the message is not received, the sender has exited.
func (r *Receiver[T]) Next() (T, bool) {
v, ok := <-r.values
return v, ok
}
// finalize will notify the sender to stop sending when the Receiver is destroyed.
func (r *Receiver[T]) finalize() {
close(r.done)
}
|
See the next subsection for usage examples.
Container Definition
With generic types, we can define type-safe containers. Instead of converting types back and forth as we did before.
Here we provide an implementation of an ordered map.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
// The orderedmaps package implements ordered dictionaries based on binary trees.
package orderedmaps
import "chans"
// Map is an ordered dictionary.
type Map[K, V any] struct {
root *node[K, V]
compare func(K, K) int
}
// node is a node in a binary tree.
type node[K, V any] struct {
k K
v V
left, right *node[K, V]
}
// New Constructs a new dictionary.
// Because the type parameter V is only used in the return value, it cannot be confirmed by type inference.
// So the type of all type parameters must be specified when calling the New function.
func New[K, V any](compare func(K, K) int) *Map[K, V] {
return &Map[K, V]{compare: compare}
}
// find looks up the node of k from the dictionary. If it exists, return the corresponding pointer.
// If it does not exist, return the location where k needs to be saved.
func (m *Map[K, V]) find(k K) **node[K, V] {
pn := &m.root
for *pn != nil {
switch cmp := m.compare(k, (*pn).k); {
case cmp < 0:
pn = &(*pn).left
case cmp > 0:
pn = &(*pn).right
default:
return pn
}
}
return pn
}
// Insert adds a new K-V.
// Existing values will be overwritten.
// Return true if it is a new key.
func (m *Map[K, V]) Insert(k K, v V) bool {
pn := m.find(k)
if *pn != nil {
(*pn).v = v
return false
}
*pn = &node[K, V]{k: k, v: v}
return true
}
// Find the value corresponding to k, or return the null value corresponding to V if it does not exist.
// If k does not exist, the second argument returns false.
func (m *Map[K, V]) Find(k K) (V, bool) {
pn := m.find(k)
if *pn == nil {
var zero V // Note the use of null values
return zero, false
}
return (*pn).v, true
}
// keyValue k-v pair used when traversing the dictionary
type keyValue[K, V any] struct {
k K
v V
}
// InOrder returns a mid-order traversal iterator. Supports concurrent operations.
func (m *Map[K, V]) InOrder() *Iterator[K, V] {
type kv = keyValue[K, V] // Specify generic parameters to simplify type references
sender, receiver := chans.Ranger[kv]()
var f func(*node[K, V]) bool
f = func(n *node[K, V]) bool {
if n == nil {
return true
}
// If sender.Send returns false then stop sending the send.
// because the receiver has stopped working at that point.
return f(n.left) &&
sender.Send(kv{n.k, n.v}) &&
f(n.right)
}
go func() {
f(m.root)
sender.Close()
}()
return &Iterator[K, V]{receiver}
}
// Iterator is used to iterate through an ordered dictionary.
type Iterator[K, V any] struct {
r *chans.Receiver[keyValue[K, V]]
}
// Next returns the next key-value pair. If the bool value is false, the iteration is complete.
func (it *Iterator[K, V]) Next() (K, V, bool) {
kv, ok := it.r.Next()
return kv.k, kv.v, ok
}
|
Examples of use are as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import "container/orderedmaps"
var m = orderedmaps.New[string, string](strings.Compare)
// Add Add k-v
func Add(a, b string) {
m.Insert(a, b)
}
i := m.InOrder()
for {
k, v, ok := i.Next()
if !ok {
break
}
// ...
}
|
Append Functions
Go has a built-in append function that allows you to append elements to arbitrary type slices. However, if Go supports generics from the start, there is no need to introduce this built-in function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// Append appends element t to the end total of slice s, returning the appended slice.
// If s has enough capacity, it is expanded in place; otherwise it is allocated and a new slice is returned.
func Append[T any](s []T, t ...T) []T {
lens := len(s)
tot := lens + len(t)
if tot < 0 {
panic("Append: cap out of range")
}
if tot > cap(s) {
news := make([]T, tot, tot + tot/2)
copy(news, s)
s = news
}
s = s[:tot]
copy(s[lens:], t)
return s
}
|
Use the following method.
1
2
|
s := slices.Append([]int{1, 2, 3}, 4, 5, 6)
// The effect is equivalent to s := append([]int{1, 2, 3}, 4, 5, 6)
|
Linked List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
// lists implements a linked List that supports elements of arbitrary type.
package lists
// List is a linked List object.
type List[T any] struct {
head, tail *element[T]
}
// element Holds information about the elements of the Linked List
type element[T any] struct {
next *element[T]
val T
}
// Push appends an element to the end of the Linked List.
func (lst *List[T]) Push(v T) {
if lst.tail == nil {
lst.head = &element[T]{val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element[T]{val: v}
lst.tail = lst.tail.next
}
}
// Iterator supports iterating over the elements of a Linked List.
type Iterator[T any] struct {
next **element[T]
}
// Range returns an iterative object
func (lst *List[T]) Range() *Iterator[T] {
return Iterator[T]{next: &lst.head}
}
// Next moves the iterator to the next meta mutual.
// Return false if it has reached the end.
func (it *Iterator[T]) Next() bool {
if *it.next == nil {
return false
}
it.next = &(*it.next).next
return true
}
// Val returns the content of the current element.
// If the element is empty bool value is false.
func (it *Iterator[T]) Val() (T, bool) {
if *it.next == nil {
var zero T
return zero, false
}
return (*it.next).val, true
}
// Transform iterates through each element of the linked List, executes function f to get the new element, and saves
// into the new Linked List, and return the new Linked List.
func Transform[T1, T2 any](lst *List[T1], f func(T1) T2) *List[T2] {
ret := &List[T2]{}
it := lst.Range()
for {
if v, ok := it.Val(); ok {
ret.Push(f(v))
}
if !it.Next() {
break
}
}
return ret
}
|
Examples of use are as follows.
1
2
3
4
5
6
|
l := lists.List[int]{}
l.Push(1)
l.Push(2)
l2 := Transform(l, func(i int) float64 { return float64(i) })
// Now the value of l2 is [1.0, 2.0]
|