Знакомство с функциональным программированием в Python, JavaScript и Java

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

Как же можно половчее переключиться от ООП к ФП?

Сегодня мы изучим ключевые принципы функционального программирования, рассмотрим их реализацию в Python, JavaScript и Java, а также прикинем, в каком направлении лучше всего продолжать двигаться.

По ходу статьи мы ответим на следующие вопросы:

  • Что такое функциональное программирование?
  • Как оно реализовано в различных языках?
  • Каковы его основные принципы?
  • Как оно используется в Python, JavaScript и Java?
  • Что стоит изучать далее?

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

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

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

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

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

Визуальное представление функций в ФП

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

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

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

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

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

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

Функциональная парадигма поддерживается не во всех языках. Некоторые из них, например Haskell, спроектированы именно для этой задачи, в то время как другие, например JavaScript, реализуют возможности и ООП, и ФП. Есть же и такие языки, где функциональное программирование невозможно в принципе.

Функциональные языки:

  • Haskell: это наиболее популярный язык среди функциональных программистов. В нем реализована защита памяти, отличный сбор мусора, а также повышенная скорость, обусловленная ранней компиляцией машинного кода. Его богатая статическая система типов дает вам доступ к уникальным алгебраическим и полиморфным типам, которые делают процесс программирования более эффективным, а код более читаемым. 
  • Erlang: этот язык, как и его потомок, Elixir, заняли нишу лучших функциональных языков для параллельных систем. Несмотря на то, что в популярности он уступает Haskell, его нередко используют для бэкенд-программирования. В последнее время Erlang начал завоевывать внимание в сфере масштабируемых мессенджеров, таких как WhatsApp и Discord.
  • Clojure: это ориентированный на функциональную парадигму диалект Lisp, который работает на виртуальной машине Java (JVM). Будучи преимущественно функциональным языком, он поддерживает как изменяемые, так и неизменяемые структуры данных, но при этом все же менее строг в функциональном плане, чем другие. Если вам нравится Lisp, то вы также полюбите и Clojure.
  • F#: этот язык аналогичен Haskell (они находятся в одной языковой группе), но имеет меньше расширенных возможностей. Кроме того, в нем реализована слабая поддержка объектно-ориентированных конструкций.

Языки с функциональными возможностями

  • Scala: этот язык поддерживает как ООП, так и ФП. Его наиболее интересная особенность в наличии строгой системы статической типизации, как в Haskell, которая помогает создавать строгие функциональные программы. При проектировании Scala среди прочих стояла задача решить многие критические проблемы Java, поэтому данный язык очень подходит для Java-разработчиков, желающих попробовать функциональное программирование. 
  • JavaScript: несмотря на то, что приоритет в этом языке не на стороне функциональной парадигмы, JavaScript уделяет ей немало внимания в связи со своей асинхронной природой. В нем также поддерживаются такие важные функциональные возможности, как лямбда выражения и деструктуризация. Вместе эти атрибуты выделяют JS как ведущий язык для ФП.
  • Python, PHP, C++: эти мультипарадигмальные языки тоже поддерживают функциональное программирование, но уже в меньшей степени, чем Scala и JavaScript.
  • Java: этот язык относится к языкам общего назначения, но приоритет в нем отдается ООП, основанному на классах. Несмотря на то, что добавление лямбда выражений в некотором смысле помогает реализовывать более функциональный стиль, в конечном итоге Java остается языком ООП. Он позволяет заниматься функциональным программированием, но при этом в нем недостает ключевых элементов, которые бы оправдывали его освоение именно с этой целью. 

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

Переменные и функции

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

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

Для чистых функций характерны два свойства:

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

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

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

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

Неизменяемость и состояния

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

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

Рекурсия

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

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

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

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

Функции высшего порядка

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

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

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

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

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

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

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

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

Чистые и неизменяемые функции

Многие из встроенных в Python структур данных являются неизменяемыми по умолчанию:

  • integer;
  • float;
  • Boolean;
  • string;
  • Unicode;
  • tuple.

Кортежи особенно полезны при использовании в качестве неизменяемой формы массива.

# код Python для проверки неизменяемости кортежей

tuple1 = (0, 1, 2, 3)  
tuple1[0] = 4
print(tuple1)

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

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

def add_1(x):
    return x + 1

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

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

Функции в качестве объектов

def shout(text): 
    return text.upper()

Передача функции в качестве параметра

def shout(text): 
    return text.upper()

def greet(func): 
    # сохраняем функцию в переменной 
    greeting = func("Hi, I am created by a function passed as an argument.") 
    print greeting  

greet(shout)

Возвращение функции из другой функции

def create_adder(x): 
    def adder(y): 
        return x+y     

return adder

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

Для компоновки функций в Python мы используем вызов lambda function. Это позволяет нам единовременно вызывать любое число аргументов.

import functools

def compose(*functions):
    def compose2(f, g):
        return lambda x: f(g(x))
    return functools.reduce(compose2, functions, lambda x: x)

На строке 4 мы определяем функцию compose2, получающую две функции в качестве аргументов f и g.
На строке 5 мы возвращаем новую функцию, представляющую композицию из f и g.

В завершении на строке 6 мы возвращаем результаты этой композиции функций.

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

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

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

Чистые и неизменяемые функции

Чтобы начать создание чистых функций в JS, нам понадобится использовать функциональные альтернативы стандартному поведению, такие как const, concat и filter.

Стандартное ключевое слово let определяет изменяемую переменную. Если вместо него для объявления использовать const, это гарантирует нам неизменность переменной, так как переназначить ее уже не получится. 

const heightRequirement = 46;

function canRide (height){
    return height >= heightRequirement;
}

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

Но у нас есть его функциональный эквивалент  —  concat(). Вот он уже возвращает новый массив, который содержит все начальные элементы вместе с добавленным. В этом случае сам начальный массив остается неизменным.

const a = [1, 2]
const b = [1, 2].concat(3)

Для удаления элемента из массива мы обычно используем методы pop() и slice(). Тем не менее они не относятся к функциональным, так как изменяют именно первичный массив. Вместо них мы берем метод filter(), который создает новый массив со всеми элементами, прошедшими проверку условия.

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.filter(word => word.length > 6);

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

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

Присвоение функции к переменной

const f = (m) => console.log(m)
f('Test')

Добавление функции в массив

const a = [
  m => console.log(m)
]
a[0]('Test')

Передача функции в качестве аргумента

const f = (m) => () => console.log(m)
const f2 = (f3) => f3()
f2(f('Test'))

Возвращение функции из другой функции

const createF = () => {
  return (m) => console.log(m)
}
const f = createF()
f('Test')

Функциональная композиция

В JavaScript мы можем компоновать функции при помощи цепочек вызовов:

obj.doSomething()
   .doSomethingElse()

В качестве альтернативы можно передать выполнение функции в следующую функцию:

obj.doSomething(doThis())

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

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

import { compose } from 'lodash/fp'

const slugify = compose(
  encodeURIComponent,
  join('-'),
  map(toLowerCase),
  split(' ')
)

slufigy('Hello World') // hello-world

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

Java очень ограниченно поддерживает ФП по сравнению с Python или JS. Тем не менее в нем есть возможность имитировать функциональное поведение при помощи лямбда функций, потоков и анонимных классов.

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

Чистые и неизменяемые функции

В Java есть несколько неизменяемых структур данных:

  • integer;
  • Boolean;
  • byte;
  • short;
  • string.

Вы также можете создавать собственные неизменяемые классы при помощи ключевого слова final.

// неизменяемый класс
public final class Student 
{ 
    final String name; 
    final int regNo;     public Student(String name, int regNo) 
    { 
        this.name = name; 
        this.regNo = regNo; 
    } 
    public String getName() 
    { 
        return name; 
    } 
    public int getRegNo() 
    { 
        return regNo; 
    } 
}

Ключевое слово final в классе предотвращает создание дочернего класса. Использование final для name и regNo делает невозможным изменение значений после построения объекта.

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

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

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

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

// ФУНКЦИЯ ПЕРВОГО КЛАССА
Supplier<String> lambda = myObject::toString;
// ФУНКЦИЯ ВЫСШЕГО ПОРЯДКА
Supplier<String> higherOrder(Supplier<String> fn) {
    String result = fn.get();
    return () -> result;
}

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

Java содержит интерфейс, java.util.function.Function, предоставляющий методы для композиции функций. Метод compose сначала выполняет переданную ему функцию (multiplyByTen), а затем передает возвращаемое значение внешней функции (square).

И наоборот  —  метод andThen выполняет сначала внешнюю функцию, а затем функцию из своих параметров.

Function<Integer, Integer> square = (input) -> input * input;
Function<Integer, Integer> multiplyByTen = (input) -> input * 10;

// COMPOSE: аргумент будет выполнен в начале
Function<Integer, Integer> multiplyByTenAndSquare = square.compose(multiplyByTen);

// ANDTHEN: аргумент будет выполнен в конце
Function<Integer, Integer> squareAndMultiplyByTen = square.andThen(multiplyByTen);

На строках 1 и 2 мы сначала создаем две функции, square и multiplyByTen.
Затем на строках 5 и 8 мы делаем из них две композиции, multiplyByTenAndSquare и squareAndMultiplyByTen, каждая из которых принимает два аргумента (удовлетворяя условие square).

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

Что изучать дальше

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

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

Успехов вам в обучении!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи The Educative Team: Functional Programming Explained in Python, JavaScript, and Java

Предыдущая статьяАвтоматизируйте код-ревью и ускорьте итерации
Следующая статьяВыход из тени: 6 малоизвестных команд Linux