
GO - НЕ ФУНКЦИОНАЛЬНЫЙ ЯЗЫК!
Как бы то ни было, я все равно буду это делать только потому, что ...
Итак, что такое Reduce
Представьте себе массив object с N количеством записей. Вы хотите вычислить значения sum и average в массиве и начинаете писать код imperatively /
sum := 0 arr := [...]int{1, 3, 5, 7, 11} for _, num := range arr { sum += num }avg := float32(sum) / float32(len(arr))
На первый взгляд, трудно понять, что происходит fuck при использовании цикла для преобразования данных. Sum вычисляется внутри цикла, а avg вычисляется снаружи. Это может быть даже более сложным, если преобразование данных является сложным и включает в себя несколько условий ветвления.
Нам часто нужно прочитать весь код внутри цикла, просто чтобы узнать его намерение.
Уменьшить в Go
Представьте себе функцию с именем Reduce. Принимает 3 параметра:
Arrayлюбого объекта. Назовем этоsourceInitial Valueкак следует из названия, начальное значение ожидаемого результатаFunction, который принимает 3 параметра и возвращает значение. Назовем этоreducer
//Reduce an array of something into another thing
func Reduce(source, initialValue, reducer interface{}) (interface{}, error) {
//TODO:
}
Теперь вы можете спросить: почему бы не использовать типизированную функцию для reducer?
// Reducer is a typed function which takes 3 parameter and must return a value
type Reducer func(accumulator, entry interface{}, idx int) interface{}
Теперь посмотрим на код ниже
notFun := Reducer(func(accumulator string, entry int, idx int) string {
return fmt.Sprintln(accumulator, entry)
})
Это даст вам ошибку компиляции:
cannot convert func literal (type func(string, int, int) string) to type Reducer
Эта жесткая проверка типов - это не то, чего мы хотим. Мы хотим, чтобы функция reducer принимала любой тип accumulator, entry и возвращала все, что нам нужно.
ТОДО-1. Убедитесь, что тип источника
srcV := reflect.ValueOf(source)
kind := srcV.Kind()
if kind != reflect.Slice && kind != reflect.Array {
return nil, errors.New("Source value is not an array")
}
ТОДО-2. Убедитесь, что reducer не равен нулю и является функцией
if reducer == nil { return nil, errors.New("Reducer function cannot be nil") }rv := reflect.ValueOf(reducer) if rv.Kind() != reflect.Func { return nil, errors.New("Reducer argument must be a function") }
TODO-3 Подготовка контейнера результатов
// copy initial value object
accumulator := initialValue
accV := reflect.ValueOf(initialValue)
TODO-4 Создание цикла и возврат результата
// for each entry in source array for i := 0; i < arrV.Len(); i++ { entry := arrV.Index(i)// call reducer via reflection, reduceResults := rv.Call([]reflect.Value{ accV, // send accumulator value entry, // send current source entry reflect.ValueOf(i), // send current loop index })// get the first index of its result, and store it as accumulator value accV = reduceResults[0] }return accV.Interface(), nil
Сшить все вместе
// Reducer Error Collection var ( ErrSourceNotArray = errors.New("Source value is not an array") ErrReducerNil = errors.New("Reducer function cannot be nil") ErrReducerNotFunc = errors.New("Reducer argument must be a function") )// Reduce an array of something into another thing func Reduce(source, initialValue, reducer interface{}) (interface{}, error) { srcV := reflect.ValueOf(source) kind := srcV.Kind() if kind != reflect.Slice && kind != reflect.Array { return nil, ErrSourceNotArray }if reducer == nil { return nil, ErrReducerNil }rv := reflect.ValueOf(reducer) if rv.Kind() != reflect.Func { return nil, ErrReducerNotFunc }// copy initial value as accumulator, and get the reflection value accumulator := initialValue accV := reflect.ValueOf(accumulator) for i := 0; i < srcV.Len(); i++ { entry := srcV.Index(i)// call reducer via reflection reduceResults := rv.Call([]reflect.Value{ accV, // send accumulator value entry, // send current source entry reflect.ValueOf(i), // send current loop index })accV = reduceResults[0] }return accV.Interface(), nil }
Обеспечение поведения Reduce с помощью модульного теста
func TestReduce(t *testing.T) { type Person struct { Name string Birthplace string } type PersonGroup map[string][]string type SumAvg struct { Sum int Avg float32 }type args struct { source interface{} initialValue interface{} reducer interface{} }sumOfInt := func(accumulator, entry, idx int) int { return accumulator + entry }avgOfInt := func(accumulator SumAvg, entry, idx int) SumAvg { sum := accumulator.Sum + entry return SumAvg{ Sum: sum, Avg: float32(sum) / float32(idx+1), } }groupBirthplacesByName := func(accumulator PersonGroup, entry Person, idx int) PersonGroup { birthplaces, exists := accumulator[entry.Name] if !exists { birthplaces = []string{entry.Birthplace} } else { birthplaces = append(birthplaces, entry.Birthplace) } accumulator[entry.Name] = birthplaces return accumulator }tests := []struct { name string args args want interface{} wantErr bool }{ { name: "Source must be an array", args: args{source: "something"}, wantErr: true, }, { name: "Reducer must not be nil", args: args{source: []int{1, 2, 3}, reducer: nil}, wantErr: true, }, { name: "Reducer must be a function", args: args{source: []int{1, 2, 3}, reducer: "something"}, wantErr: true, }, { name: "Sum of array", args: args{ source: []int{1, 2, 3}, initialValue: 0, reducer: sumOfInt, }, wantErr: false, want: 6, }, { name: "Avg of array", args: args{ source: []int{1, 2, 3}, initialValue: SumAvg{Sum: 0, Avg: 0}, reducer: avgOfInt, }, wantErr: false, want: SumAvg{ Sum: 6, Avg: 6 / 3, }, }, { name: "Group by person's name", args: args{ source: []Person{ Person{"John Doe", "Jakarta"}, Person{"John Doe", "Depok"}, Person{"John Doe", "Medan"}, }, initialValue: make(PersonGroup), reducer: groupBirthplacesByName, }, wantErr: false, want: PersonGroup{"John Doe": []string{"Jakarta", "Depok", "Medan"}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Reduce(tt.args.source, tt.args.initialValue, tt.args.reducer) if (err != nil) != tt.wantErr { t.Errorf("Reduce() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Reduce() = %v, want %v", got, tt.want) } }) } }
И результаты тестов показывают:
Running tool: /usr/local/opt/go/libexec/bin/go test -timeout 30s github.com/bastianrob/arrayutil -run ^(TestReduce)$ok github.com/bastianrob/arrayutil 0.005s Success: Tests passed.
Вывод
Используя сокращение:
- Мы пытаемся разбить проблему на более мелкие части
- Таким образом, он может поместиться в одну функцию редуктора
- Который может быть назван
Nколичество раз - Чтобы произвести один результат
- Вместо того, чтобы делать все это в одном императивном цикле
Также приятно, что мы можем:
- Прочтите имя функции редуктора
- Получите быстрый взгляд на его намерение
- Без необходимости читать весь код внутри
- И уверен, что он дает правильный результат, когда функция редуктора должным образом протестирована
Вся эта махинация у меня на гитхабе
Иконки сделаны https://www.flaticon.com/authors/freepik