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

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

16 марта 2022 г.

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


Чистые функции


Это базовая сущность функционального программирования. Из вики:


возвращаемые функции идентичны для идентичных аргументов


Вот пример чистой функции:


```иди


функция умножить (a, b int) int {


вернуть а * б


Функционирует как гражданин первого класса


Согласно вики, это означает, что:


данный объект (например, функция) поддерживает все операционные свойства, присущие другим объектам; такие свойства, как возможность присваиваться переменной, передаваться в качестве аргумента функции, возвращаться из функции и т. д.


Мы можем вернуть функцию:


```иди


основной пакет


импортировать "фмт"


основная функция () {


myFunc := makeCounter()


мояФункция()


мояФункция()


функция makeCounter() функция() {


счетчик := 0


функция возврата() {


fmt.Println(счетчик)


счетчик++


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


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


Упаковка


На момент написания этой статьи Go не поддерживает универсальные типы до версии Go 1.18, которая будет включать поддержку универсальных типов и должна быть выпущена в марте 2022 г.


Go 1.18 еще не выпущен. Это незавершенные примечания к выпуску. Ожидается, что Go 1.18 будет выпущен в марте 2022 года.


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


```иди


функция умножить (a, b int) int {


вернуть а * б


func wrapWithLogs (fn func (a, b int) int) func (a, b int) int {


return func(a, b int) int {


начало := время.Сейчас()


г := fn(a, b)


продолжительность: = время. С (начало)


fmt.Println("функция выполнена", продолжительность)


вернуть р


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


Если вы хотите получить имя функции динамически, вы также можете использовать:


```иди


время выполнения.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()


Обтекание открывает двери для использования некоторых интересных вещей, таких как запоминание.


Мемоизация


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


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


```иди


func sum(a,b int) int {


вернуть а + б


func memo(fn func(a, b int) int) func(a, b int) int {


кеш: = сделать (карта [строка] целое)


return func(a, b int) int {


ключ := strconv.Itoa(a) + " " + strconv.Itoa(b)


v, хорошо := кэш[ключ]


если! хорошо {


fmt.Println("расчет...")


кеш[ключ] = fn(a, b)


v = кеш [ключ]


вернуть v


основная функция () {


mSum := памятка (сумма)


fmt.Println(mSum(2, 3))


fmt.Println(mSum(2, 3))


Результат будет:


:::Информация


расчет... 5 5


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


Рекурсия



Мы можем использовать рекурсию, если нам нужно вычислить что-то вроде факториала:


```иди


func funcFactorial(num int) int {


если число == 0 {


вернуть 1


вернуть число * funcFactorial (число-1)


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


Каррирование


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


```иди


функция умножить (a, b int) int {


вернуть а * б


Мы можем назвать эту функцию как «умножить (2, 3)». С каррированием мы можем вызвать эту функцию как multiply(2)(3). Для этого нам нужно переписать нашу основную функцию:


```иди


функция умножить (a int) функция (b int) int {


функция возврата (b int) int {


вернуть а * б



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


```иди


функция умножить (a, b int) int {


вернуть а * б


func curry(a int) func(b int) int {


функция возврата (b int) int {


вернуть умножить (а, б)


Заключение


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



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE