Статьи

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

Вступление

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

Java по своей сути является императивным языком программирования. Однако в недавнем прошлом стали популярными многие новые языки, такие как Scala, Clojure, Groovy и т. Д., Которые поддерживают стиль функционального программирования и все же работают на JVM. Однако ни один из этих языков не может рассматриваться как чисто функциональный язык, поскольку все они позволяют вызывать код Java изнутри, и сама по себе Java не является функциональным языком. Тем не менее, они имеют разную степень поддержки написания кода в функциональном стиле и имеют свои преимущества. Функциональное программирование требует другого типа мышления и имеет свои преимущества по сравнению с императивным программированием.

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

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

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

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

Основанные на Java функциональные языки программирования, такие как Scala, Clojure, Groovy и т. Д., Рассматривают эти проблемы под другим углом зрения и предоставляют менее сложные и менее подверженные ошибкам решения по сравнению с императивным программированием. Они предоставляют концепции неизменяемости из коробки и, следовательно, устраняют необходимость синхронизации и связанный с этим риск взаимоблокировок или блокировок. Такие понятия, как переменные Actors, Agents и DataFlow, обеспечивают высокоуровневую абстракцию параллелизма и упрощают написание параллельных программ.

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

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

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

1. Функции могут быть переданы внутри функций в качестве аргументов.

2. Функции могут быть созданы внутри функций так же, как объекты могут быть созданы в функциях

3. Функции могут быть возвращены из функций

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

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

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

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

  • Scala
  • Clojure
  • Groovy
  • Лямбда-выражения в Java 8

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

Scala

Scala — это типизированный язык программирования с несколькими парадигмами, разработанный для интеграции функций объектно-ориентированного программирования и функционального программирования. Поскольку он статический, нельзя изменить определение класса во время выполнения, то есть нельзя добавлять новые методы или переменные во время выполнения. Однако Scala предоставляет концепции функционального программирования, такие как неизменяемость, функции высшего порядка, вложенные функции и т. Д. Помимо поддержки модели параллелизма Java, она также предоставляет концепцию модели Actor из коробки для основанной на событиях асинхронной передачи сообщений между объектами. Код, написанный на Scala, компилируется в очень эффективный байт-код, который затем может быть выполнен на JVM.

Создать неизменный список в Scala очень просто и не требует дополнительных усилий. Ключевое слово «val» делает свое дело.

val numbers = List(1,2,3,4)

Функции могут быть переданы в качестве аргументов. Давайте посмотрим на это на примере.

Предположим, у нас есть список из 10 чисел, и мы хотим вычислить сумму всех чисел в списке.

val numbers = List(1,2,3,4,5,6,7,8,9,10)

val total = numbers.foldLeft(0){(a,b) =>

    a+b

}

Как видно из приведенного выше примера, мы передаем функцию для добавления двух переменных «a» и «b» в другую функцию «foldLeft», которая предоставляется библиотекой Scala для коллекций. Мы также не использовали никакой итерационной логики и временной переменной для вычисления суммы. Метод «foldLeft» устраняет необходимость поддерживать состояние во временной переменной, что в противном случае потребовалось бы, если бы мы писали этот код чисто Java-способом (как упомянуто ниже).

int total = 0;

for(int number in numbers){

    total+=number;

}

Функция Scala может легко выполняться параллельно без какой-либо синхронизации, поскольку она не изменяет состояние.

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

Clojure

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

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

(def numbers (list 1 2 3 4 5 6 7 8 9 10))

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

(reduce + 0 '(1 2 3 4 5 6 7 8 9 10))

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

Groovy

Groovy снова является динамическим языком с некоторой поддержкой функционального программирования. Из 3 языков Groovy можно считать самым слабым с точки зрения функциональных возможностей программирования. Однако из-за его динамической природы и близкого сходства с Java, он получил широкое признание и считается хорошей альтернативой Java. Groovy не предоставляет неизменных объектов из коробки, но имеет отличную поддержку для функций более высокого порядка. Неизменяемые объекты можно создавать с помощью аннотации @Immutation, но она гораздо менее гибка, чем поддержка неизменяемости в Scala и Clojure. В Groovy функции могут передаваться так же, как и любая другая переменная в форме замыканий. Тот же пример в Groovy можно записать следующим образом

def numbers = [1,2,3,4,5,6,7,8,9,10]

def total = numbers.inject(0){a,b ->

    a+b

}

Однако следует отметить, что переменные «числа» и «общее» не являются неизменяемыми и могут быть изменены в любой момент времени. Следовательно, написание многопоточного кода может быть немного сложным. Но Groovy предоставляет концепцию переменных Actors, Agents и DataFlow через библиотеку под названием GPars (Groovy Parallel System), которая в большей степени уменьшает проблемы, связанные с многопоточным кодом.

Java8 лямбда-выражение

Java наконец-то осознала всю мощь написания кода в функциональном стиле и собирается поддержать концепцию замыканий, начиная с Java8. JSR 335 — лямбда-выражения для языка программирования JavaTM предназначены для поддержки программирования в многоядерной среде путем добавления замыканий и связанных с ними функций в язык Java. Таким образом, наконец-то можно будет передавать функции, подобные переменным в чистом коде Java.

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

ExecutorService executor = Executors.newCachedThreadPool();

executor.submit(() -> {System.out.println("I am running")})

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

Вывод

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