Java.next: Преодоление синонимических трудностей

Распознавание сходных функциональных конструктов в языках Java.net

В предыдущей статье цикла Java.next, которая называется ("Стили написания функционального кода"), сравнивались сходные и различные моменты стилей функционального кодирования на языках Scala, Groovy, Clojure. В этой статье автор цикла Нил Форд более подробно рассматривает реализацию функций filter, map и reduce в языках Java.next. Серия коротких примеров кодирования помогает читателю разобраться в сбивающих с толку различиях в названиях этих ключевых функциональных конструктов в языках Java.next.

Нил Форд, Архитектор приложений, ThoughtWorks

Нил Форд (Neal Ford) работает архитектором приложений в ThoughtWorks, революционной компании, предоставляющей профессиональные IT-услуги и помогающей талантливым людям по всему миру эффективнее использовать программное обеспечение. Он также является проектировщиком и разработчиком приложений, учебных материалов, журнальных статей, учебных курсов, видео/DVD-презентаций и автором книг "Разработка с Delphi: Объектно-ориентированный подход", "JBuilder 3 Unleashed" и "Искусство разработки Web-приложений на Java". Он специализируется на консультациях по построению широкомасштабных корпоративных приложений. Он также является общепризнанным докладчиком и выступал на многочисленных конференциях разработчиков по всему миру. С ним можно связаться по адресу: nford@thoughtworks.com.



31.03.2014

Об этом цикле статей

Java-технология оставит в наследство не язык, а платформу. На платформе JVM работают более 200 языков программирования; какой-то из них со временем неминуемо вытеснит язык Java в качестве наилучшего способа программирования для JVM. Этот цикл статей посвящен исследованию трех языков нового поколения для платформы JVM — Groovy, Scala и Clojure — а также сравнению и противопоставлению новых возможностей и парадигм, чтобы дать Java-разработчикам возможность заглянуть в свое ближайшее будущее.

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

В этой статье я продолжаю рассматривать практическое применение трех ключевых функций (filter, map, reduce) и демонстрирую подробности их реализации в каждом языке Java.next. Мои объяснения в сочетании с приведенными в статье примерами призваны устранить путаницу, которая может иметь место в результате несогласованности имен, используемых в этих трех языках для аналогичных функциональных конструктов.

Filter

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

Scala

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

val numbers = List.range(1, 11)
numbers filter (x => x % 3 == 0)
// List(3, 6, 9)

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

numbers filter (_ % 3 == 0)
// List(3, 6, 9)

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

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

val words = List("the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog")
words filter (_.length == 3)
// List(the, fox, the, dog)

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

numbers partition (_ % 3 == 0)
// (List(3, 6, 9),List(1, 2, 4, 5, 7, 8, 10))

Функция filter() возвращает коллекцию соответствующих элементов, а функция find() возвращает только первый соответствующий критерию элемент.

numbers find (_ % 3 == 0)
// Some(3)

Однако возвращаемое функцией find() значение — это не сам соответствующий критерию элемент, а этот элемент, обернутый в класс Option. Класс Option имеет два возможных значения: Some или None. Scala, как и некоторые другие функциональные языки, использует Option в качестве соглашения, позволяющего избежать возвращения Option в качестве соглашения, позволяющего избежать возвращения null в случае отсутствия искомого значения. Экземпляр Some() обертывает фактическое возвращаемое значение, которое составляет 3 в случае numbers find (_ % 3 == 0). Если я попытаюсь найти нечто несуществующее, будет возвращено значение None:

numbers find (_ < 0)
// None

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

List(1, 2, 3, -4, 5, 6, 7, 8, 9, 10) takeWhile (_ > 0)
// List(1, 2, 3)

Функция dropWhile() пропускает наибольшее количество элементов, которые удовлетворяют предикату.

words dropWhile (_ startsWith "t")
// List(quick, brown, fox, jumped, over, the, lazy, dog)

Groovy

Язык Groovy не считается функциональным, но тем не менее содержит множество функциональных парадигм, некоторые из которых имеют названия, взятые из скриптовых языков. Например, функции, которая в функциональных языках традиционно носит имя filter(), в Groovy соответствует метод findAll():

(1..10).findAll {it % 3 == 0}
// [3, 6, 9]

Как и filter-функции в Scala, этот Groovy-метод работает со всеми типами, включая строки.

def words = ["the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"]
words.findAll {it.length() == 3}
// [The, fox, the, dog]

Кроме того, в Groovy имеется partition()()-подобная функция под названием split():

(1..10).split {it % 3}
// [[1, 2, 4, 5, 7, 8, 10], [3, 6, 9]]

Возвращаемое методом split() значение представляет собой вложенный массив, подобный вложенному списку, который возвращает функция partition() в Scala.

Groovy-метод find() возвращает первый подходящий элемент из коллекции.

(1..10).find {it % 3 == 0}
// 3

В отличие от Scala, Groovy следует соглашениям Java и возвращает null в тех случаях, когда методу find() не удается найти искомый элемент.

(1..10).find {it < 0}
// null

Кроме того, в Groovy имеются методы takeWhile() и dropWhile(), семантика которых подобна соответствующим Scala-версиям.

[1, 2, 3, -4, 5, 6, 7, 8, 9, 10].takeWhile {it > 0}
// [1, 2, 3]
words.dropWhile {it.startsWith("t")}
// [quick, brown, fox, jumped, over, the, lazy, dog]

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

def moreWords = ["the", "two", "ton"] + words
moreWords.dropWhile {it.startsWith("t")}
// [quick, brown, fox, jumped, over, the, lazy, dog]

Clojure

В Clojure имеется поразительное количество процедур для манипулирования коллекциями. Многие из них имеют весьма обобщенный характер по причине динамической типизации языка Clojure. Многие разработчики обращаются к языку Clojure по причине богатства и гибкости его библиотек коллекций. Clojure использует традиционные имена функционального программирования, как наглядно демонстрирует функция (filter ):

(def numbers (range 1 11))
(filter (fn [x] (= 0 (rem x 3))) numbers)
; (3 6 9)

Как многие другие языки, Clojure имеет краткий синтаксис для простых анонимных функций:

(filter #(zero? (rem % 3)) numbers)
; (3 6 9)

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

(def words ["the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog"])
(filter #(= 3 (count %)) words)
; (the fox the dog)

Clojure-функция (filter ) возвращает результат типа Seq, заключенный в круглые скобки. Seq — это базовая абстракция для последовательных коллекций в Clojure.


Map

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

Scala

В Scala функция map() принимает фрагмент кода и возвращает преобразованную коллекцию.

List(1, 2, 3, 4, 5) map (_ + 1)
// List(2, 3, 4, 5, 6)

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

words map (_.length)
// List(3, 5, 5, 3, 6, 4, 3, 4, 3)

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

List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9)) flatMap (_.toList)
// List(1, 2, 3, 4, 5, 6, 7, 8, 9)

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

words flatMap (_.toList)
// List(t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, ...

Groovy

Язык Groovy также включает несколько разновидностей функции map под общим названием collect(). В варианте по умолчанию эта функция принимает фрагмент кода для применения к каждому элементу коллекции.

(1..5).collect {it += 1}
// [2, 3, 4, 5, 6]

Как и другие языки, Groovy допускает сокращенное написание для простых анонимных функций высшего порядка; зарезервированное слово it замещает собой одиночный параметр.

Метод collect() работает с любой коллекцией, к которой можно применить осмысленный предикат, например, со списком строк.

def words = ["the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"]
words.collect {it.length()}
// [3, 5, 5, 3, 6, 4, 3, 4, 3]

В Groovy также имеется метод, подобный функции flatMap(); это метод под названием flatten() свертывает внутреннюю структуру. :

[[1, 2, 3], [4, 5, 6], [7, 8, 9]].flatten()
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

Кроме того, метод flatten() работает и с неочевидными коллекциями, такими как строки.

(words.collect {it.toList()}).flatten()
// [t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, j, ...

Clojure

В Clojure имеется функция (map ), которая принимает функцию высшего порядка (содержащую операторы) и коллекцию.

(map inc numbers)
; (2 3 4 5 6 7 8 9 10 11)

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

(map #(count %) words)
; (3 5 5 3 6 4 3 4 3)

Clojure-функция (flatten ) подобна аналогичной Groovy-функции.

(flatten [[1 2 3] [4 5 6] [7 8 9]])
; (1 2 3 4 5 6 7 8 9)

Fold/reduce

Третья распространенная функция в трех языках Java.next имеет наибольшее количество вариаций с точки зрения наименования, а также множество тонких различий. Функции foldLeft и reduce— это специфические разновидности для концепции манипулирования списками под названием катаморфизм (catamorphism), которая является обобщением свертывания списка. В данном случае функция fold left имеет следующий смысл:

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

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

Scala

Язык Scala обладает самым богатым набором операций типа fold. Отчасти это объясняется необходимостью поддержания нескольких сценариев типизации, которая благодаря динамической типизации отсутствует в Groovy и в Clojure. Операция Reduce обычно используется для выполнения суммирования.

List.range(1, 10) reduceLeft((a, b) => a + b)
// 45

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

List.range(1, 10).reduceLeft(0)(_ + _)
// 45

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

List.range(1, 10) reduceRight(_ - _)
// 5

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

words.reduceLeft((a, b) => if (a.length > b.length) a else b)
// jumped

Операции reduce и fold имеют перекрывающуюся функциональность, а рассмотрение тонких различий между ними выходит за рамки данной статьи. Тем не менее есть одно очевидное и широко используемое различие. В Scala сигнатура вида reduceLeft[B >: A](op: (B, A) => B): B показывает, что ожидаемым параметром является только функция для объединения элементов. Предполагается, что исходное значение будет первым значением в коллекции. В отличие от нее, сигнатура вида foldLeft[B](z: B)(op: (B, A) => B): B указывает начальное значение для получения результата, что позволяет возвращать типы, отличающиеся от типа элементов списка.

В следующем примере осуществляется суммирование коллекции с использованием операции foldLeft:

List.range(1, 10).foldLeft(0)(_ + _)
// 45

Scala поддерживает перегрузку операторов; соответственно две распространенные fold-операции, foldLeft и foldRight, имеют соответствующие им операторы: /: and :\. Это позволяет создать более краткую версию sum с помощью foldLeft:

(0 /: List.range(1, 10)) (_ + _)
// 45

Аналогичным образом для вычисления каскадной разности с каждым элементом списка (операция, обратная суммированию, потребность в которой, как правило, возникает довольно редко), можно использовать функцию foldRight() или оператор :\:

(List.range(1, 10) :\ 0) (_ - _)
// 5

Groovy

Groovy в категории reduce использует перегрузку для поддержания такой же функциональности, как у Scala-опций reduce() и foldLeft(). Одна из версий этой функции принимает начальное значение. В следующем примере метод inject() применяется для генерации суммы коллекции.

(1..10).inject {a, b -> a + b}
// 55

Альтернативная форма этого метода принимает начальное значение.

(1..10).inject(0, {a, b -> a + b})
// 55

Библиотека функций у Groovy намного меньше, чем у Scala или у Clojure — что неудивительно с учетом того, что Groovy является мультипарадигменным языком, а не языком, ориентированным на функциональное программирование.

Clojure

Clojure является языком преимущественно функционального программирования и соответственно поддерживает функцию (reduce ). В качестве опции функция (reduce ) принимает начальное значение, благодаря чему она охватывает оба случая, реализуемые Scala-функциями reduce() иfoldLeft(). Функция (reduce ) ведет себя ожидаемым образом. Она принимает функцию, которая предполагает получение двух параметров, и коллекцию.

(reduce + (range 1 11))
; 55

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


Заключение

Трудности изучения иной парадигмы (такой как функциональное программирование) отчасти обусловлены необходимостью освоения новой терминологии. Задача дополнительно усложняется в случае, когда различные сообщества используют различные термины. Однако если вы осознаете общие черты, то увидите, что все три языка Java.next предлагают перекрывающуюся функциональность, хотя и удивительными с точки зрения синтаксиса способами.

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

Ресурсы

Научиться

  • Оригинал статьи: Java.next: Overcome synonym suffering.
  • Scala: современный функциональный язык, работающий на платформе JVM.
  • Groovy: динамическая разновидность Java с обновленным синтаксисом и расширенными возможностями.
  • Clojure: современный функциональный вариант Lisp, работающий на платформе JVM.
  • reducers: эта мощная библиотека для Clojure реализует автоматическое распараллеливание операций reduce.
  • Функциональное мышление: колонка Нила Форда на developerWorks, посвященная функциональному программированию.
  • Записная книжка дизайнера языка: в этом цикле статей developerWorks архитектор языка Java Брайан Гетц исследует некоторые проблемы дизайна языка, вызвавшие трудности при эволюции языка Java версий Java SE 7, Java SE 8 и далее.
  • Раздел developerWorks, посвященный Java-технологии: сотни статей по всем аспектам программирования на Java.

Получить продукты и технологии

Обсудить

  • Присоединяйтесь к сообществу Get involved in the developerWorks. Связывайтесь с другими пользователями developerWorks и знакомьтесь с ориентированными на разработчиков форумами, блогами, группами и вики-ресурсами.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


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

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=967297
ArticleTitle=Java.next: Преодоление синонимических трудностей
publish-date=03312014