01 сентября 2022

⚒️ Зачем использовать функциональное программирование, если есть ООП?

iOS-developer, ИТ-переводчица, пишу статьи и гайды.
Легко писать, легко отлаживать и использовать повторно. Правда ли это? Давайте разбираться.
⚒️ Зачем использовать функциональное программирование, если есть ООП?
Данная статья является переводом. Ссылка на оригинал.

Функциональное программирование — достаточно актуальная тема. В опросе разработчиков 2021 года, проведенном Stack Overflow, функциональные языки были признаны одними из самых востребованных. Популярные библиотеки JavaScript, такие как React и Angular, позволяют использовать функциональные концепции в ваших компонентах, классические объектно-ориентированные языки добавили поддержку функционального программирования... но все же возникла некоторая путаница в отношении того, что на самом деле означает функциональное программирование.

Обычно люди считаются, что это концепция, которую нужно изучить при переходе на более продвинутый уровень разработки, но это не обязательно так!

«Я думаю, что функциональное программирование доступнее для тех, кто только пытается научиться программировать. Я видел, как люди из самых разных слоев общества приходили на подкаст Elixir Wizards и говорили мне, что поняли Elixir, когда только начинали его учить, благодаря оператору конвейера. Оператор ( `|>` ) облегчает новичкам понимание того, что делает их код, с четким описанием того, с чего они начали, что меняли и что получилось в конце. В целом, я думаю, что функциональное программирование больше похоже на разговорный язык».
Сунди Мьин, соведущая подкаста Elixir Wizards

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

Что такое функциональное программирование?

Есть три «типа» программирования, которые вы можете знать или не знать: процедурное программирование, объектно-ориентированное программирование и функциональное программирование. Я сосредоточусь на последних двух.

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

«Функциональное программирование является парадигмой, которая заставляет нас делать сложные части нашей системы явными, и это важный ориентир при написании программного обеспечения».
Хосе Валим, создатель Эликсира

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

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

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

Есть три рекомендации, которым вы обычно должны следовать:

  1. Ваши функции должны принимать по крайней мере один аргумент.
  2. Ваши функции должны возвращать данные или другую функцию.
  3. Не используйте циклы!

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

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека программиста»

Пример для демонстрации разницы между ООП и функциональным программированием

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

        class Student {
  constructor(name, gpa) {
    this.name = name;
    this.gpa = gpa;
  }

    getGPA() {
      return this.gpa;
  }

  changeGPA(amount) {
    return this.gpa + amount;
  }
}
    

Если вы хотите инициализировать ученика, вы можете сделать что-то вроде этого:

        let jacklyn = new Student('Jacklyn Ford', 3.95);
    

Теперь предположим, что вы хотите изменить средний балл группы студентов. С ООП у вас может быть массив учеников:

        let students = [ new Student('Jacklyn Ford', 3.95), 
new Student('Cassidy Williams', 4.0), new Student('Joe Randy', 2.2) ];

// из юридических соображений, персонажи вымышлены 
и, возможно, мой средний балл был идеальным в колледже
    

Для изменения среднего балла каждого студента, вы можете написать цикл и повысить оценки:

        for (let i = 0; i < students.length; i++) {
  students[i].changeGPA(.1);
}
    

… или что-то ещё. Затем вы можете снова выполнить цикл, чтобы вывести результаты на экран, или просто поработать с объектами по своему усмотрению.

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

        let students = [
  ['Jacklyn Ford', 3.95],
  ['Cassidy Williams', 4.0],
  ['Joe Randy', 2.2],
];
    

Студенты хранятся в виде простых массивов, а не объектов. Функциональное программирование предпочитает простые структуры данных, такие как массивы, списки и хэши (и т. д.), чтобы не «усложнять» данные и поведение. Итак, вместо того, чтобы писать только одну функцию changeGPA(), которую вы зацикливаете, у вас будет функции changeGPAs() и changeGPA().

        function changeGPAs(students) {
  return students.map(student => changeGPA(student, .1))
}

function changeGPA(student, amount) {
  return [student[0], student[1] + amount]
}
    

Функция changeGPAs() будет принимать на вход массив студентов. Затем она вызовет changeGPA() для каждого значения в массиве студентов и вернет результат в виде нового массива. Задача changeGPA() состоит в том, чтобы вернуть копию переданного учащегося с обновленным средним баллом.

Дело в том, что функциональное программирование предпочитает крошечные модульные функции, которые выполняют одну часть более крупной задачи! Работа changeGPAs() заключается в обработке массива студентов, а работа changeGPA() — в обработке каждого отдельного студента. Также обратите внимание, что исходный массив не изменяется, потому что мы рассматриваем данные как неизменяемые в функциональном программировании. Мы создаем новые наборы данных вместо изменения существующих.

Зачем мне использовать функциональное программирование?

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

«Как только вы привыкнете к этому, это становится очевидным. Понятным. Я смотрю на свою функцию. С чем она может работать? С её аргументами. Что-нибудь еще? Нет. Существуют ли глобальные переменные? Нет. Другие данные модуля? Нет. Все просто.
Роберт Вирдинг, соавтор Erlang

Отладка

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

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

Например, предположим, что у нас есть счетчик, который пропускает число 5.

        let count = 0;

function increment() {
  if (count !== 4) count += 1;
  else count += 2;

  return count
}
    

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

Между тем, если вы написали эту функцию функциональным стилем:

        function pureIncrement(count) {
  if (count !== 4) return count + 1;
  else return count + 2;
}
    

Нам не нужно запускать pureIncrement() несколько раз для проверки. Вы можете легко выполнить модульное тестирование функции, потому что она всегда будет возвращать одно и то же с одними и теми же входными данными, и никакая переменная не будет изменена (помните, неизменность)!

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

Возможность повторного использования

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

Допустим, вы хотите решить математическую задачу, например:

        (6 * 9) / ((4 + 2) + (4 * 3)) 
    

Если бы вы делали это вручную, вы могли бы решить задачу, добавив/умножив все числа, объединив то, что в скобках, а затем разделив результаты.

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

        (define (mathexample)
  (/
    (* 6 9)
    (+
      (+ 2 4)
      (* 4 3)
    )
  )
)
    

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

«<a href="https://xkcd.com/435/" target="_blank" rel="noopener noreferrer nofollow">Purity</a> » от <a href="https://xkcd.com/about/" target="_blank" rel="noopener noreferrer nofollow">xkcd</a> распространяется под лицензией <a href="https://creativecommons.org/licenses/by-nc/2.5/" target="_blank" rel="noopener noreferrer nofollow">CC BY-NC 2.5.</a>
«Purity » от xkcd распространяется под лицензией CC BY-NC 2.5.

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

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

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

Ладно, ладно, я заинтригован. Как начать?

Если вы уже хорошо разбираетесь в JavaScript или Python, вы можете сразу приступить к изучению концепций функционального программирования, о которых мы говорили здесь. Если вы хотите больше узнать о «чистых» языках, предназначенных для функционального программирования, вы можете попробовать семейство Lisp (включая Common Lisp, Scheme и Clojure), семейство ML (включая OCaml и F#), Erlang, Elixir, Elm или Haskell.

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

***

Материалы по теме

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ