Go
June 12, 2023

Функциональное программирование в Go

Почему вам стоило бы практиковать функциональное программирование с Go? Если коротко то, функциональное программирование делает ваш код более читаемым, более простым для тестирования и менее сложным из-за отсутствия состояний и изменяемых данных. Если вы обнаружите ошибки, вы можете быстро отладить свое приложение, если конечно не нарушаете правила функционального программирования. Когда функции изолированы, вам не нужно иметь дело со скрытыми изменениями состояния, которые влияют на результат.

Инженер-программист и автор Эрик Эллиот определил функциональное программирование следующим образом.

Функциональное программирование - это процесс создания программного обеспечения путем композиции чистых функций, избегая общего состояния, изменяемых данных и побочных эффектов. Функциональное программирование является декларативным, а не императивным, и состояние приложения передается через чистые функции. Отличие от объектно-ориентированного программирования, где состояние приложения обычно является общим и вместе с методами размещено в объектах.

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

4 важные концепции для понимания

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

  1. Чистые функции и идемпотентность
  2. Побочные эффекты
  3. Композиция функций
  4. Общее состояние и неизменяемые данные

Давайте быстро рассмотрим.

1. Чистые функции и идемпотентность

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

2. Побочные эффекты

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

Например, функциональное программирование рассматривает вызов API как побочный эффект. Почему? Потому что вызов API считается внешней средой, которая не находится под вашим непосредственным контролем. API может иметь несколько несоответствий, таких как тайм-аут или сбой, или он может даже возвращать неожиданное значение. Это не соответствует определению чистой функции, поскольку нам требуются согласованные результаты при каждом вызове API.

Другие распространенные побочные эффекты включают:

  • Мутацию данных
  • Манипуляции с DOM
  • Запрос конфликтующих данных, таких как текущий DateTimeс time.Now()

3. Композиция функций

Основная идея композиции функций проста: вы объединяете две чистые функции для создания новой функции. Это означает, что концепция получения одного и того же вывода для одного и того же ввода по-прежнему применяется здесь. Поэтому важно создавать более продвинутые функциональные возможности, начиная с простых, чистых функций.

4. Общее состояние и неизменяемые данные

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

Однако не все состояния плохие. Иногда состояние необходимо для решения определенной проблемы программного обеспечения. Цель функционального программирования - сделать состояние видимым и явным для устранения любых побочных эффектов. Программа использует неизменяемые структуры данных для получения новых данных из использования чистых функций. Таким образом, нет необходимости в изменяемых данных, которые могут вызвать побочные эффекты.

Теперь, когда мы рассмотрели наши основы, давайте определим некоторые правила, которым следует следовать при написании функционального кода в Go.

Правила функционального программирования

Как я уже упоминал, функциональное программирование - это парадигма. Таким образом, трудно определить точные правила для этого стиля программирования. Также не всегда возможно следовать этим правилам до конца; иногда вам действительно нужно полагаться на функцию, которая содержит состояние.

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

  • Нет изменяемых данных, чтобы избежать побочных эффектов
  • Нет состояния (или неявного состояния, такого как счетчик циклов)
  • Не изменяйте переменные после того, как им присвоено значение
  • Избегайте побочных эффектов, таких как вызов API

Один хороший “побочный эффект”, с которым мы часто сталкиваемся в функциональном программировании, - это сильная модульность. Вместо того, чтобы подходить к разработке программного обеспечения сверху вниз, функциональное программирование поощряет восходящий стиль программирования. Начните с определения модулей, которые группируют похожие чистые функции, которые, как вы ожидаете, понадобятся в будущем. Далее, начните писать эти небольшие, независимые функции без состояния, чтобы создать свои первые модули.

5 Примеров функционального программирования на Go

Чтобы нарисовать более полную картину того, как работает функциональное программирование на Go, давайте рассмотрим пять основных примеров.

1. Обновление строки

Это самый простой пример чистой функции. Обычно, когда вы хотите обновить строку, вы должны сделать следующее.

name := "first name"
name := name + " last name"

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

Приведенный ниже фрагмент кода гораздо более удобочитаем.

firstname := "first"
lastname := "last"
fullname := firstname + " " + lastname

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

2. Избегайте обновления массивов

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

В нефункциональном программировании обновите массив следующим образом:

names := [3]string{"Tom", "Ben"}

// Add Lucas to the array
names[2] = "Lucas"

Давайте попробуем это в соответствии с парадигмой функционального программирования.

names := []string{"Tom", "Ben"}
allNames := append(names, "Lucas")

В примере используется исходный namesфрагмент в сочетании с append()функцией для добавления дополнительных значений в новый массив.

3. Избегайте обновления карт

Это несколько более экстремальный пример функционального программирования. Представьте, что у нас есть карта с ключом типа string и значением типа integer. В карте фиксируется количество фруктов, которые у нас еще остались дома. Тем не менее, мы только что купили apple и хотим добавить его в список.

fruits := map[string]int{"bananas": 11}


// Buy five apples
fruits["apples"] = 5

Мы можем выполнить ту же функциональность в рамках парадигмы функционального программирования.

fruits := map[string]int{"bananas": 11}
newFruits := map[string]int{"apples": 5}

allFruits := make(map[string]int, len(fruits) + len(newFruits))


for k, v := range fruits {
    allFruits[k] = v
}

for k, v := range newFruits {
    allFruits[k] = v
}

Поскольку мы не хотим изменять исходные карты, код перебирает обе карты и добавляет значения в новую карту. Таким образом, данные остаются неизменными.

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

4. Функции более высокого порядка и каррирование

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

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

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

func add(x int) func(y int) int {
    return func(y int) int {
        return x + y
    }
}

Теперь давайте попробуем каррирование и создадим более продвинутые чистые функции.

func main() {
    // Create more variations
    add10 := add(10)
    add20 := add(20)

    // Currying
    fmt.Println(add10(1)) // 11
    fmt.Println(add20(1)) // 21
}

Этот подход распространен в функциональном программировании, хотя вы не часто видите его вне парадигмы.

5. Рекурсия

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

Например, приведенный ниже фрагмент кода пытается вычислить факториал для числа. Факториал является произведением целого числа и всех целых чисел до него. Итак, факториал 4 равен 24 (= 4 * 3 * 2 * 1).

Обычно для этого используется цикл.

func factorial(fac int) int {
    result := 1
    for ; fac > 0; fac-- {
        result *= fac
    }
    
    return result
}

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

func calculateFactorial(fac int) int {
    if fac == 0 {
        return 1
    }
    
    return fac * calculateFactorial(fac - 1)
}

Заключение

Давайте подведем итог тому, что мы узнали о функциональном программировании:

  • Хотя Golang поддерживает функциональное программирование, он не был разработан для этой цели, о чем свидетельствует отсутствие таких функций, как Map, Filter и Reduce
  • Функциональное программирование улучшает читаемость вашего кода, поскольку функции являются чистыми и, следовательно, простыми для понимания
  • Чистые функции легче тестировать, поскольку нет внутреннего состояния, которое может изменить результат

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

Для лучшего обзора различий между функциональным, процедурным и объектно-ориентированным программированием, или если вы хотите понять, какая парадигма подходит вам лучше всего, я рекомендую прочитать этот проницательный пост Medium от Lili Ouaknin Felsen.

Источник: https://blog.logrocket.com/functional-programming-in-go