Skip to main content
Go: Arrays and Slices with Generics
  1. Posts/

Go: Arrays and Slices with Generics

Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

Following the Learn Go with Tests guide, revisiting arrays and slices with Go’s generics system.

This post builds upon previous exploration of arrays and slices in Go.

Original Implementation
#

Basic implementations of Sum and SumAllTails functions:

// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
	var sum int
	for _, number := range numbers {
		sum += number
	}
	return sum
}
// SumAllTails calculates the sums of all but the first number
// given a collection of slices.
func SumAllTails(numbersToSum ...[]int) []int {
	var sums []int
	for _, numbers := range numbersToSum {
		if len(numbers) == 0 {
			sums = append(sums, 0)
		} else {
			tail := numbers[1:]
			sums = append(sums, Sum(tail))
		}
	}

	return sums
}

Reduce Pattern
#

Looking at the Sum function, might notice a recurring pattern:

  1. Create an initial result value (e.g., var sum int)
  2. Iterate over the collection, applying an operation to the result and each item
  3. Return the accumulated result

This pattern is known as reduce (or fold) in functional programming.

Reduce Implementation
#

Functional Programming in Go: Reduce/fold originates from functional languages. Go now supports these patterns through:

  • Generics (v1.18+) for type-safe abstractions
  • First-class functions - functions can be passed as arguments, returned as values, and stored in variables

Implementation of generic Reduce function:

func Reduce[A any](collection []A, f func(A, A) A, initialValue A) A {
	var result = initialValue
    // Start with the initial value
	for _, x := range collection {
		result = f(result, x)
        // Apply combining function
	}
	return result
    // Return final accumulated result
}

Now refactor Sum function to use Reduce

func Sum(numbers []int) int {
	add := func(acc, x int) int {
		return acc + x
	}
	return Reduce(numbers, add, 0)
}

And update SumAllTails

func SumAllTails(numbersToSum ...[]int) []int {
	sumTail := func(acc, x []int) []int {
		if len(x) == 0 {
			acc = append(acc, 0)
		} else {
			tail := x[1:]
			acc = append(acc, Sum(tail))
		}
		return acc
	}

	return Reduce(numbersToSum, sumTail, []int{})
}

Testing Reduce
#

Test with different data types and operations.

Testing with multiplication:

func TestReduce(t *testing.T) {
	t.Run("multiplication of all elements", func(t *testing.T) {
		multiply := func(x, y int) int {
			return x * y
		}
		AssertEqual(t, Reduce([]int{1, 2, 3}, multiply, 1), 6)
	})

Testing with string concatenation:

	t.Run("concatenate strings", func(t *testing.T) {
		concatenate := func(x, y string) string {
			return x + y
		}

		result := Reduce([]string{"a", "b", "c"}, concatenate, "")
		AssertEqual(t, result, "abc")
	})
}

Reducing to Different Types
#

Sometimes we need to reduce a collection into a different type than the collection elements.

Example test

func TestBadBank(t *testing.T) {
	transactions := []Transaction{
		{
			From: "Chris",
			To:   "Riya",
			Sum:  100,
		},
		{
			From: "Adil",
			To:   "Chris",
			Sum:  25,
		},
	}

	AssertEqual(t, BalanceFor(transactions, "Riya"), 100)
	AssertEqual(t, BalanceFor(transactions, "Chris"), -75)
	AssertEqual(t, BalanceFor(transactions, "Adil"), -25)
}

Transaction struct:

type Transaction struct {
	From string
	To   string
	Sum  float64
}

The traditional approach to calculate balances:

func BalanceFor(transactions []Transaction, name string) float64 {
	var balance float64
	for _, t := range transactions {
		if t.From == name {
			balance -= t.Sum
		}
		if t.To == name {
			balance += t.Sum
		}
	}
	return balance
}

Enhanced Reduce
#

Current Reduce function is limited because it requires the accumulator and collection elements to be the same type. If we tried to use the original Reduce function for our banking example, we’d get a compile error:

./bank.go:32:30: in call to Reduce, type func(currentBalance float64, t Transaction) float64 of adjustBalance does not match inferred type func(Transaction, Transaction) Transaction for func(A, A) A

Need a more flexible version:

func Reduce_v2[A, B any](  
    // A = collection element type
    // B = result type
	collection []A, 
	f func(B, A) B, 
	initialValue B,
) B {
	var result = initialValue
	for _, x := range collection {
		result = f(result, x)
	}
	return result
}

Two generic types (A, B) allow the accumulator type to differ from the collection element type. This enables reducing:

  • []Transaction into float64
  • []string into int
  • etc.

Now can implement BalanceFor using reduce

func BalanceForReduce(transactions []Transaction, name string) float64 {
	adjustBalance := func(
		currentBalance float64, 
		t Transaction,
	) float64 {
		if t.From == name {
			return currentBalance - t.Sum
		}
		if t.To == name {
			return currentBalance + t.Sum
		}
		return currentBalance
	}
	return Reduce_v2(transactions, adjustBalance, 0.0)
}