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 любого объекта. Назовем это source
  • Initial 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