> For the complete documentation index, see [llms.txt](https://eda-1.gitbook.io/lgwt/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://eda-1.gitbook.io/lgwt/osnovy-go/revisiting-arrays-and-slices-with-generics.md).

# Пересмотр массивов и срезов с дженериками

[**Код для этой главы является продолжением раздела «Массивы и срезы» и находится здесь**](https://github.com/quii/learn-go-with-tests/tree/main/arrays)

Взгляните на функции `SumAll` и `SumAllTails`, которые мы написали в главе [массивы и срезы](/lgwt/osnovy-go/arrays-and-slices.md). Если у вас нет вашей версии, скопируйте код из главы [массивы и срезы](/lgwt/osnovy-go/arrays-and-slices.md) вместе с тестами.

```go
// 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' (редукция) или [fold](https://en.wikipedia.org/wiki/Fold_\(higher-order_function\)) (свёртка).

> В функциональном программировании свёртка (также называемая редукция, накопление, агрегация, сжатие или инъекция) относится к семейству функций высшего порядка, которые анализируют рекурсивную структуру данных и, используя заданную объединяющую операцию, рекомбинируют результаты рекурсивной обработки её составных частей, создавая возвращаемое значение. Обычно свёртка принимает объединяющую функцию, корневой узел структуры данных и, возможно, некоторые значения по умолчанию, которые будут использоваться при определённых условиях. Затем свёртка систематически объединяет элементы иерархии структуры данных, используя эту функцию.

В Go всегда были функции высшего порядка, а с версии 1.18 появились ещё и [дженерики](/lgwt/osnovy-go/generics.md), поэтому теперь стало возможным определять некоторые из этих функций, обсуждаемых в нашей более широкой области. Нет смысла прятать голову в песок, это очень распространённая абстракция за пределами экосистемы Go, и будет полезно её понять.

Я знаю, что некоторые из вас, вероятно, морщатся от этого.

> Go должен быть простым

**Не путайте лёгкость с простотой**. Делать циклы и копировать-вставлять код легко, но это не обязательно просто. Чтобы узнать больше о простоте против лёгкости, посмотрите [шедевральную лекцию Рича Хики — Simple Made Easy](https://www.youtube.com/watch?v=SxdOUGdseq4).

**Не путайте незнакомость со сложностью**. Свёртка/редукция поначалу может звучать пугающе и по-компьютерному, но на самом деле это всего лишь абстракция над очень распространённой операцией. Взять коллекцию и объединить её в один элемент. Отступив назад, вы поймёте, что, вероятно, делаете это *очень часто*.

## Обобщённый рефакторинг

Распространённая ошибка, которую люди допускают с новыми возможностями языка, заключается в том, что они начинают использовать их, не имея конкретного сценария использования. Они полагаются на предположения и догадки, чтобы направлять свои усилия.

К счастью, мы написали наши "полезные" функции и обложили их тестами, поэтому теперь мы можем свободно экспериментировать с идеями на стадии рефакторинга TDD и знать, что всё, что мы пробуем, имеет подтверждение своей ценности с помощью наших модульных тестов.

Использование дженериков в качестве инструмента для упрощения кода на этапе рефакторинга гораздо чаще приводит к полезным улучшениям, нежели к преждевременным абстракциям.

Мы можем смело пробовать что-то, повторно запускать наши тесты; если нам нравятся изменения, мы можем их зафиксировать. Если нет, просто отменить изменения. Эта свобода экспериментировать — одна из поистине огромных ценностей TDD.

Вы должны быть знакомы с синтаксисом дженериков [из предыдущей главы](/lgwt/osnovy-go/generics.md), попробуйте написать свою собственную функцию `Reduce` и использовать её внутри `Sum` и `SumAllTails`.

### Подсказки

Если вы сначала подумаете об аргументах вашей функции, это даст вам очень небольшой набор допустимых решений:

* Массив, который вы хотите свернуть
* Некая объединяющая функция

"Reduce" — невероятно хорошо задокументированный шаблон, нет необходимости изобретать велосипед. [Прочитайте вики, в частности раздел о списках](https://en.wikipedia.org/wiki/Fold_\(higher-order_function\)), это должно подсказать вам ещё один аргумент, который вам понадобится.

> На практике удобно и естественно иметь начальное значение.

### Моя первая версия `Reduce`

```go
func Reduce[A any](collection []A, f func(A, A) A, initialValue A) A {
	var result = initialValue
	for _, x := range collection {
		result = f(result, x)
	}
	return result
}
```

`Reduce` улавливает *суть* шаблона: это функция, которая принимает коллекцию, функцию-аккумулятор, начальное значение и возвращает одно значение. Здесь нет запутанных отвлекающих факторов, связанных с конкретными типами.

Если вы понимаете синтаксис дженериков, у вас не должно возникнуть проблем с пониманием того, что делает эта функция. Используя общепринятый термин `Reduce`, программисты из других языков также понимают её назначение.

### Использование

```go
// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
	add := func(acc, x int) int { return acc + x }
	return Reduce(numbers, add, 0)
}

// SumAllTails calculates the sums of all but the first number given a collection of slices.
func SumAllTails(numbers ...[]int) []int {
	sumTail := func(acc, x []int) []int {
		if len(x) == 0 {
			return append(acc, 0)
		} else {
			tail := x[1:]
			return append(acc, Sum(tail))
		}
	}

	return Reduce(numbers, sumTail, []int{})
}
```

`Sum` и `SumAllTails` теперь описывают поведение своих вычислений как функции, объявленные соответственно в их первых строках. Действие выполнения вычисления над коллекцией абстрагировано в `Reduce`.

## Дальнейшие применения редукции

Используя тесты, мы можем поэкспериментировать с нашей функцией свёртки, чтобы увидеть, насколько она многоразова. Я скопировал наши обобщённые функции утверждения из предыдущей главы.

```go
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)
	})

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

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

### Нулевое значение

В примере с умножением мы показываем причину наличия значения по умолчанию в качестве аргумента `Reduce`. Если бы мы полагались на значение по умолчанию в Go (0 для `int`), мы бы умножили наше начальное значение на 0, а затем и последующие, так что вы всегда получали бы 0. Установив его на 1, первый элемент в срезе останется таким же, а остальные будут умножаться на следующие элементы.

Если вы хотите показаться умным перед своими друзьями-ботаниками, вы бы назвали это [Нейтральным элементом](https://en.wikipedia.org/wiki/Identity_element).

> В математике нейтральный элемент бинарной операции, действующей на множестве, является элементом множества, который оставляет неизменным каждый элемент множества при применении операции.

В случае сложения нейтральный элемент — это 0.

`1 + 0 = 1`

В случае умножения — 1.

`1 * 1 = 1`

## Что, если мы хотим свернуть в тип, отличающийся от `A`?

Предположим, у нас был список транзакций `Transaction`, и мы хотели бы иметь функцию, которая принимала бы их и имя, чтобы определить баланс счёта.

Давайте следовать процессу TDD.

## Сначала напишите тест

```go
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)
}
```

## Попробуйте запустить тест

```
# github.com/quii/learn-go-with-tests/arrays/v8 [github.com/quii/learn-go-with-tests/arrays/v8.test]
./bad_bank_test.go:6:20: undefined: Transaction
./bad_bank_test.go:18:14: undefined: BalanceFor
```

## Напишите минимальное количество кода, чтобы тест запустился и проверьте вывод ошибочного теста

У нас пока нет наших типов или функций, добавьте их, чтобы тест запустился.

```go
type Transaction struct {
	From string
	To   string
	Sum  float64
}

func BalanceFor(transactions []Transaction, name string) float64 {
	return 0.0
}
```

При запуске теста вы должны увидеть следующее:

```
=== RUN   TestBadBank
    bad_bank_test.go:19: got 0, want 100
    bad_bank_test.go:20: got 0, want -75
    bad_bank_test.go:21: got 0, want -25
--- FAIL: TestBadBank (0.00s)
```

## Напишите достаточно кода, чтобы он прошёл

Давайте сначала напишем код, как если бы у нас не было функции `Reduce`.

```go
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
}
```

## Рефакторинг

На этом этапе проявите дисциплину контроля версий и зафиксируйте свою работу. У нас есть работающее программное обеспечение, готовое бросить вызов Monzo, Barclays и другим.

Теперь, когда наша работа зафиксирована, мы можем свободно экспериментировать с ней и пробовать различные идеи на этапе рефакторинга. Честно говоря, наш код не так уж плох, но ради этого упражнения я хочу продемонстрировать тот же код, используя `Reduce`.

```go
func BalanceFor(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(transactions, adjustBalance, 0.0)
}
```

Но это не скомпилируется.

```
./bad_bank.go:19:35: type func(acc float64, t Transaction) float64 of adjustBalance does not match inferred type func(Transaction, Transaction) Transaction for func(A, A) A
```

Причина в том, что мы пытаемся свернуть в *другой* тип, нежели тип коллекции. Это звучит пугающе, но на самом деле просто требует от нас скорректировать сигнатуру типа `Reduce`, чтобы она работала. Нам не придётся менять тело функции, и нам не придётся менять ни один из наших существующих вызывающих объектов.

```go
func Reduce[A, B any](collection []A, f func(B, A) B, initialValue B) B {
	var result = initialValue
	for _, x := range collection {
		result = f(result, x)
	}
	return result
}
```

Мы добавили второе ограничение типа, что позволило нам ослабить ограничения на `Reduce`. Это позволяет сворачивать коллекцию типа `A` в тип `B`. В нашем случае из `Transaction` в `float64`.

Это делает `Reduce` более универсальной и многоразовой, и при этом типобезопасной. Если вы попробуете запустить тесты снова, они должны скомпилироваться и пройти.

## Расширение банка

Ради забавы я хотел улучшить эргономику банковского кода. Я опустил процесс TDD для краткости.

```go
func TestBadBank(t *testing.T) {
	var (
		riya  = Account{Name: "Riya", Balance: 100}
		chris = Account{Name: "Chris", Balance: 75}
		adil  = Account{Name: "Adil", Balance: 200}

		transactions = []Transaction{
			NewTransaction(chris, riya, 100),
			NewTransaction(adil, chris, 25),
		}
	)

	newBalanceFor := func(account Account) float64 {
		return NewBalanceFor(account, transactions).Balance
	}

	AssertEqual(t, newBalanceFor(riya), 200)
	AssertEqual(t, newBalanceFor(chris), 0)
	AssertEqual(t, newBalanceFor(adil), 175)
}
```

А вот обновлённый код:

```go
package main

type Transaction struct {
	From string
	To   string
	Sum  float64
}

func NewTransaction(from, to Account, sum float64) Transaction {
	return Transaction{From: from.Name, To: to.Name, Sum: sum}
}

type Account struct {
	Name    string
	Balance float64
}

func NewBalanceFor(account Account, transactions []Transaction) Account {
	return Reduce(
		transactions,
		applyTransaction,
		account,
	)
}

func applyTransaction(a Account, transaction Transaction) Account {
	if transaction.From == a.Name {
		a.Balance -= transaction.Sum
	}
	if transaction.To == a.Name {
		a.Balance += transaction.Sum
	}
	return a
}
```

Я считаю, что это действительно показывает мощь использования таких концепций, как `Reduce`. `NewBalanceFor` кажется более *декларативным*, описывающим *что* происходит, а не *как*. Часто, когда мы читаем код, мы просматриваем множество файлов и пытаемся понять *что* происходит, а не *как*, и этот стиль кода хорошо этому способствует.

Если я захочу углубиться в детали, я могу это сделать и увидеть *бизнес-логику* `applyTransaction`, не беспокоясь о циклах и изменяющемся состоянии; `Reduce` занимается этим отдельно.

### Свёртка/редукция довольно универсальны

Возможности `Reduce` (или `Fold`) безграничны™️. Это распространённый шаблон по уважительной причине, он подходит не только для арифметики или конкатенации строк. Попробуйте несколько других применений.

* Почему бы не смешать несколько `color.RGBA` в один цвет?
* Подсчитать общее количество голосов в опросе или предметов в корзине покупок.
* Более или менее всё, что связано с обработкой списка.

## Поиск (Find)

Теперь, когда в Go появились дженерики, объединив их с функциями высшего порядка, мы можем значительно сократить количество шаблонного кода в наших проектах, чтобы наши системы было легче понимать и поддерживать.

Вам больше не нужно писать специальные функции `Find` для каждого типа коллекции, которую вы хотите найти; вместо этого используйте или напишите одну функцию `Find`. Если вы поняли функцию `Reduce` выше, написание функции `Find` будет тривиальным.

Вот тест:

```go
func TestFind(t *testing.T) {
	t.Run("find first even number", func(t *testing.T) {
		numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

		firstEvenNumber, found := Find(numbers, func(x int) bool {
			return x%2 == 0
		})
		AssertTrue(t, found)
		AssertEqual(t, firstEvenNumber, 2)
	})
}
```

А вот реализация:

```go
func Find[A any](items []A, predicate func(A) bool) (value A, found bool) {
	for _, v := range items {
		if predicate(v) {
			return v, true
		}
	}
	return
}
```

Опять же, поскольку он принимает обобщённый тип, мы можем повторно использовать его по-разному:

```go
type Person struct {
	Name string
}

t.Run("Find the best programmer", func(t *testing.T) {
	people := []Person{
		Person{Name: "Kent Beck"},
		Person{Name: "Martin Fowler"},
		Person{Name: "Chris James"},
	}

	king, found := Find(people, func(p Person) bool {
		return strings.Contains(p.Name, "Chris")
	})

	AssertTrue(t, found)
	AssertEqual(t, king, Person{Name: "Chris James"})
})
```

Как видите, этот код безупречен.

## В заключение

При правильном использовании такие функции высшего порядка сделают ваш код более простым для чтения и поддержки, но помните эмпирическое правило:

Используйте процесс TDD для выявления реального, специфического поведения, которое вам действительно необходимо; на стадии рефакторинга вы *можете* обнаружить некоторые полезные абстракции, которые помогут привести код в порядок.

Практикуйте сочетание TDD с хорошими привычками контроля версий. Фиксируйте свою работу, когда ваш тест проходит, *прежде* чем пытаться рефакторить. Таким образом, если вы что-то испортите, вы легко сможете вернуться в рабочее состояние.

### Имена имеют значение

Постарайтесь провести небольшое исследование за пределами Go, чтобы не изобретать заново уже существующие шаблоны с уже устоявшимися названиями.

Пишете функцию, которая берёт коллекцию `A` и преобразует её в `B`? Не называйте её `Convert`, это [`Map`](https://en.wikipedia.org/wiki/Map_\(higher-order_function\)). Использование «правильных» названий для этих элементов уменьшит когнитивную нагрузку для других и сделает её более удобной для поиска информации.

### Это не кажется идиоматичным?

Постарайтесь быть непредвзятыми.

Хотя идиомы Go не будут и не должны *радикально* меняться из-за выпуска дженериков, идиомы *будут* меняться — из-за изменения языка! Это не должно быть спорным моментом.

Сказать

> Это не идиоматично

Без каких-либо дополнительных деталей — это не действенное и не полезное утверждение. Особенно при обсуждении новых возможностей языка.

Обсуждайте с коллегами шаблоны и стиль кода, основываясь на их достоинствах, а не на догмах. Пока у вас есть хорошо разработанные тесты, вы всегда сможете рефакторить и изменять вещи по мере того, как вы поймёте, что хорошо работает для вас и вашей команды.

### Ресурсы

Свёртка (Fold) — это настоящий фундаментальный концепт в информатике. Вот несколько интересных ресурсов, если вы хотите углубиться в него:

* [Википедия: Свёртка](https://ru.wikipedia.org/wiki/Свёртка_списка)
* [Учебное пособие по универсальности и выразительности свёртки](http://www.cs.nott.ac.uk/~pszgmh/fold.pdf)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://eda-1.gitbook.io/lgwt/osnovy-go/revisiting-arrays-and-slices-with-generics.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
