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

Есть известная цитата из Чистого кода Боба Мартина о том, что отношение чтения кода к написанию кода составляет десять к одному. Если это хотя бы близко к истине, и если мы вообще заботимся об эффективности, то любой выигрыш, который мы можем сделать в нашей способности читать код, окупится с лихвой, поскольку у нас будет больше времени для написания кода. Кто не хочет написать больше кода?!

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

Все начинается с чистых функций

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

Это простой способ сказать, что он детерминирован. Он также не имеет побочных эффектов: таких вещей, как изменение состояния объекта или запись в файл. Для примера в Scala:

var baseNumber: Int = 10

def impureAdd(numberToAdd: Int): Int = baseNumber += numberToAdd

def pureAdd(baseNumber: Int, numberToAdd: Int): Int = baseNumber + numberToAdd

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

Итак, какое отношение это имеет к удобочитаемости и как это помогает нам писать лучший код?

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

Это приводит меня к:

Предложение 1. Передайте все зависимости вашим функциям

Вы также можете передавать функции своим функциям

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

def someFunction(numberOne: Int, numberTwo: Int): Int =
  numberOne + numberTwo

def myImpureFunction(numberOne: Int, numberTwo: Int): Int =
  someFunction(numberOne, numberTwo)

def myPureFunction(numberOne: Int, numberTwo: Int, fn: (Int, Int) => Int): Int =
  fn(numberOne, numberTwo)

Хотя этот пример не имеет функционального смысла (каламбур), он иллюстрирует то, в чем я определенно был очень виновен в прошлом. Вы можете представить себе ситуацию: мы написали длинную функцию и хотим, чтобы все было СУХИМ (не повторяйтесь для непосвященных), поэтому мы вытаскиваем какую-то общую операцию или код, который имеет единственную ответственность, в свою собственную функцию. Это оставляет нас в той же ситуации, что и на плохом примере. Теперь мы связаны с конкретной реализацией, и мы должны помнить об обеих функциях при чтении этого кода.

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

def add(numberOne: Int, numberTwo: Int): Int = numberOne + numberTwo

val resultOne: Int = firstClass(1, 2, add)

val resultTwo: Int = add(1, 2)

val resultThree: Int = 1 + 2

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

Предложение 2. Передавайте свои функции точно так же, как и данные

Но мы не всегда можем использовать чистые функции

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

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

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

По сути, монады позволяют нам построить программу, в которой для нашего ядра мы всегда можем предположить, что у нас есть то, с чем нам нужно работать. Нам не нужно определять поток управления или проверять nil как часть нашего приложения или бизнес-логики. Два замечательных примера монад в Scala — это монады Option и Either, обе из которых часто используются для упрощения наличия или отсутствия данных и/или ошибок, которые могут возникнуть во время обработки.

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

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

Предложение 3. Оберните побочные эффекты и данные

Предложение 4. Создайте свою сторону, влияющую на компоненты на границе вашей системы

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

Первоначально опубликовано на https://www.reasontree.io.