Общий обзор основных концепций обоих и того, как их можно эффективно комбинировать.

Если вы получили обычное образование в области компьютерных наук, скорее всего, вы начали учиться программировать, используя императивный стиль, то есть «говоря» компьютеру, что именно делать и в каком порядке.

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

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

Инженеры-программисты склонны к смелым заявлениям вроде «все или ничего», «моя сторона или неправильная». Вы найдете людей с обеих сторон, которые очень непреклонны в следовании «чистой версии» избранного, и у них будут веские аргументы.

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

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

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

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

Объектно-ориентированное программирование (ООП)

1. Абстракция

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

2. Наследование

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

3. Полиморфизм

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

4. Инкапсуляция

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

Концепции функционального программирования (FP)

1. Функции высокого порядка (HOF)

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

2. Чистая функция

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

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

  • Изменение переменной за пределами текущей области
  • Изменение существующей структуры данных 
  • Установка поля на объекте 
  • Генерация исключения
  • операция ввода-вывода

3. Рекурсия

В ФП циклы представлены как рекурсивные функции.

Здесь есть особая подкатегория — хвостовая рекурсия, когда рекурсивные вызовы функции находятся в хвостовой позиции.

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

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

tailrec fun fibonacci(n: Int, a: Int = 0, b: Int = 1): Int = 
   when (n) {
      0 -> a
      1 -> b
      else -> fibonacci(n — 1, b, a + b)
   }

Если бы мы генерировали какую-то другую нечетную последовательность с вызовом типа

fun myRandomSequence(n: Int, a: Int = 0, b: Int = 1): Int = 
   when (n) {
      0 -> a
      1 -> b
      else -> myRandomSequence(n — 1, b, a + b) + 9299
   }

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

4. Строгая и нестрогая оценка

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

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

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

if (x == true || y == false)  {
  // doA()
} else if (w == true && z == true) {
  // doB()
} else {
  // doC()
}

В приведенном выше примере есть 3 языковые конструкции, которые предотвращают оценку битов кода. Булевы операторы оценивают только первый бит — or, если условие выполнено, и and, если нет. Точно так же, если условие в разделе if оценивается как истинное, будет выполнено doA, а второе условие, а также другие doX действия будут пропущены.

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

fun nonStrictIf(condition: Boolean, onTrue: () -> Unit, onFalse: () -> Unit) = if (cond) onTrue() else onFalse()
val y = nonStrictIf(x==true, { println(“x is true”) }, { println(“x is false”) } )

5. Ссылочная прозрачность (RT)

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

Сравнение Резюме

В очень общих чертах это различие между двумя методологиями:

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

Получение лучшего из обоих миров

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

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

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

Что еще? Многие языки, обычно называемые ООП, являются не только ООП, но и чисто ООП — Kotlin, Scala и даже Java теперь постепенно добавляют кусочки из FP. Итак, теперь речь идет не об ООП и ФП, а об их совместном использовании для достижения наиболее эффективного, надежного и чистого приложения.

Спасибо за прочтение!