Skip to content

Latest commit

 

History

History
282 lines (207 loc) · 18.7 KB

p10-hof.md

File metadata and controls

282 lines (207 loc) · 18.7 KB

Глава 10: Не повторяемся вместе с функциями высшего порядка

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

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

Один из простейших способов реализации новой функции заключается в вызове других функций в её теле. Но есть и другие способы. В этой статье мы обсудим базовые понятия функционального программирования. Вы узнаете о том как в лучших традициях принципа DRY(Don't Repeat Youself - не повторяйтесь) функции высшего порядка применяются для повторного использования кода.

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

Функция высшего порядка, в отличие от функции первого порядка, имеет один из трёх видов:

  1. Один из параметров функции также является функцией и она возвращает значение.

  2. Она возвращает функцию, но ни один из параметров не является функцией.

  3. И первый и второй пункт: функция возвращает функцию и один из параметров является функцией.

Нам уже встречалось множество примеров функций первого типа. Методы map, filter, flatMap -- все они принимают функции, с помощью которых мы преобразуем или фильтруем коллекции. Очень часто, мы передавали анонимные функции, что иногда приводило к дублированию.

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

Из ничего появилась функция

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

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

case class Email(
  subject: String,
  text: String,
  sender: String,
  recipient: String)

Мы хотим фильтровать сообщения на основе некоторого критерия, определённого пользователем. В итоге у нас будет функция Email => Boolean, с помощью которой мы будем фильтровать письма. Если функция вернёт истину мы принимаем письмо, в противном случае — отбрасываем.

type EmailFilter = Email => Boolean
def newMailsForUser(mails: Seq[Email], f: EmailFilter) = mails.filter(f)

Обратите внимание на то, как мы дали специальное имя нашей функции. Мы объявили новый тип-синоним для повышения читаемости кода.

Теперь мы можем создать методы-фабрики, создающие EmailFilter на основе предпочтений пользователей:

val sentByOneOf: Set[String] => EmailFilter =
  senders => email => senders.contains(email.sender)

val notSentByAnyOf: Set[String] => EmailFilter =
  senders => email => !senders.contains(email.sender)

val minimumSize: Int => EmailFilter = n => email => email.text.size >= n

val maximumSize: Int => EmailFilter = n => email => email.text.size <= n

Каждая из этих четырёх переменных возвращает EmailFilter. Первые две принимают Seq[String], представляющую набор адресатов-отправителей. Оставшиеся принимают целое число, указывающее на длину содержания письма.

С помощью любой из этих функций мы можем создать новый фильтр EmailFilter для функции newMailsForUser:

val emailFilter: EmailFilter = notSentByAnyOf(Set("johndoe@example.com"))

val mails = Email(
  subject = "It's me again, your stalker friend!",
  text = "Hello my friend! How are you?",
  sender = "johndoe@example.com",
  recipient = "me@example.com") :: Nil

newMailsForUser(mails, emailFilter) // возвращает пустой список

Этот фильтр удалит одно сообщение, поскольку пользователь решил добавить отправителя в чёрный список. Так с помощью методов-фабрик мы можем создавать самые разные фильтры сообщений EmailFilter, согласно пользовательским предпочтениям.

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

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

Для этого в функциях minimumSize и maximumSize мы воспользуемся функцией sizeConstraint. Она будет принимать предикат, определённый на длине текста письма:

type SizeChecker = Int => Boolean
val sizeConstraint: SizeChecker => EmailFilter = f => email => f(email.text.size)

Теперь мы можем выразить minimumSize и maximumSize через sizeConstraint:

val minimumSize: Int => EmailFilter = n => sizeConstraint(_ >= n)
val maximumSize: Int => EmailFilter = n => sizeConstraint(_ <= n)

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

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

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

def complement[A](predicate: A => Boolean) = (a: A) => !predicate(a)

Теперь для некоторого предиката p, мы можем построить его дополнение вызовом complement(p). Но не смотря на то что функция sentByAnyOf не является предикатом, она возвращает предикат, а именно EmailFilter.

в Scala определены две функции для композиции функций. Ими мы и воспользуемся. Если у нас есть две функции f и g, выражение f.compose(g) вернёт новую функцию, которая при вызове сначала выполнит функцию g и затем применит f к результату. Аналогично f.andThen(g) вернёт функцию, которая сначала вызовет f и затем g, на результате, который был получен из f.

Теперь мы можем определить notSentByAnyOf без дублирования:

val notSentByAnyOf = sentByOneOf andThen(g => complement(g))

В этом выражении мы определяем новую функцию через композицию двух других. Сначала мы вызываем sentByOneOf на аргументе определяемой функции, а затем передаём результат (предикат EmailFilter) в функцию complement. Мы можем сделать это выражение ещё более наглядным, воспользовавшись специальным синтаксисом, для определения функций, через подчёркивание:

val notSentByAnyOf = sentByOneOf andThen(complement(_))

Конечно Вы заметили, что с помощью функции complement мы можем определить и функцию maximumSize через minimumSize, вместо того чтобы пользоваться функцией sizeConstraint. Однако исходное определение выигрывает в гибкости. Мы можем определить много разных предикатов на основе длины текста письма.

Композиция предикатов

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

Мы можем определить функции для комбинации предикатов так:

def any[A](predicates: (A => Boolean)*): A => Boolean =
  a => predicates.exists(pred => pred(a))
def none[A](predicates: (A => Boolean)*) = complement(any(predicates: _*))
def every[A](predicates: (A => Boolean)*) = none(predicates.view.map(complement(_)): _*)

Функция any возвращает предикат, который проверяет: вернёт ли истину хотя бы один из предикатов. Функция none просто возвращает дополнение к результату функции any — если хотя бы один из предикатов вернёт истину, всё выражение вернёт ложь. И наконец функция every проверяет, что ни одно из дополнений к переданным предикатам не вернёт истину.

Теперь мы можем определять составные фильтры EmailFilter на основе предпочтений пользователя:

val filter: EmailFilter = every(
    notSentByAnyOf(Set("johndoe@example.com")),
    minimumSize(100),
    maximumSize(10000)
  )

Цепочки преобразований

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

val addMissingSubject = (email: Email) =>
  if (email.subject.isEmpty) email.copy(subject = "No subject")
  else email

val checkSpelling = (email: Email) =>
  email.copy(text = email.text.replaceAll("your", "you're"))

val removeInappropriateLanguage = (email: Email) =>
  email.copy(text = email.text.replaceAll("dynamic typing", "**CENSORED**"))

val addAdvertismentToFooter = (email: Email) =>
  email.copy(text = email.text + "\nThis mail sent via Super Awesome Free Mail")

Теперь в зависимости от погоды или настроения шефа, мы можем настроить нашу цепочку преобразований либо с помощью многократных вызовов метода andThen, либо вызовом метода chain, что определён на объекте компаньоне Function:

val pipeline = Function.chain(Seq(
  addMissingSubject,
  checkSpelling,
  removeInappropriateLanguage,
  addAdvertismentToFooter))

Функции высшего порядка и частично определённые функции

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

Композиция частично определённых функций

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

val handler = fooHandler orElse barHandler orElse bazHandler

Альтернативное представление частично определённых функций

Иногда нам хотелось бы, чтобы частично определённая функция была представлена по-другому. Ведь что такое по-сути частично определённая функция? Это функция, что возвращает Option[A]. Если она не определена, она возвращает None, иначе мы получим значение в Some[A].

Для того чтобы преобразовать PartialFunction по имени pf в такой вид, мы можем вызвать pf.lift, для обратного преобразования можно воспользоваться методом Function.unlift(f).

Итоги

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

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