В последнем посте мы узнали о неизменяемости и параллелизме. В этом разделе мы рассмотрим функции высшего порядка и замыкания.
Если вы не читали часть 2, прочтите ее здесь:
Функции высшего порядка
Функции высшего порядка - это функции, которые могут принимать функции как параметры и возвращать функции как результаты. Круто, да?
Но зачем кому-то это нужно?
Возьмем пример. Предположим, я хочу сжать кучу файлов. Я хочу сделать это двумя способами - в формате ZIP или RAR. Чтобы сделать это в традиционной Java, мы бы использовали что-то вроде паттерна стратегии.
Во-первых, я бы сделал интерфейс, определяющий стратегию:
public interface CompressionStrategy { void compress(List<File> files); }
Затем я бы реализовал две стратегии следующим образом:
public class ZipCompressionStrategy implements CompressionStrategy { @Override public void compress(List<File> files) { // Do ZIP stuff } } public class RarCompressionStrategy implements CompressionStrategy { @Override public void compress(List<File> files) { // Do RAR stuff } }
Затем во время выполнения я могу использовать одну из этих стратегий:
public CompressionStrategy decideStrategy(Strategy strategy) { switch (strategy) { case ZIP: return new ZipCompressionStrategy(); case RAR: return new RarCompressionStrategy(); } }
Это много кода и церемоний.
Все, что мы здесь пытаемся сделать, это попытаться реализовать два разных элемента бизнес-логики в зависимости от некоторой переменной. Поскольку бизнес-логика не может существовать в Java сама по себе, мы должны оформить ее в классах и интерфейсах.
Разве не было бы замечательно, если бы мы могли напрямую передать бизнес-логику? То есть, если бы мы могли рассматривать функции как переменные, могли бы мы передавать бизнес-логику так же легко, как переменные и данные?
Это именно то, для чего нужны функции высшего порядка!
Давайте посмотрим на тот же пример с функциями высшего порядка. Я собираюсь использовать здесь Kotlin, поскольку лямбды Java 8 все еще включают некоторую церемонию создания функциональных интерфейсов, которой мы бы хотели избежать.
fun compress(files: List<File>, applyStrategy: (List<File>) -> CompressedFiles){ applyStrategy(files) }
Метод compress
принимает два параметра - список файлов и функцию с именем applyStrategy
, которая является функцией типа List<File> -> CompressedFiles.
, то есть это функция, которая принимает список файлов и возвращает CompressedFiles
.
Теперь мы можем вызвать compress
с любой функцией, которая принимает список файлов и возвращает сжатые файлы:
compress(fileList, {files -> // ZIP it}) compress(fileList, {files -> // RAR it})
Лучше. Намного лучше.
Таким образом, функции высшего порядка позволяют передавать логику и обрабатывать код как данные. Аккуратный.
Закрытие
Замыкания - это функции, которые фиксируют свое окружение. Давайте разберемся в этом на примере. Предположим, у меня есть прослушиватель кликов в представлении, и мы хотим вывести внутри него какое-то значение:
int x = 5; view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println(x); } });
Java не позволяет нам этого делать, поскольку x
не является окончательным. x
должен быть окончательным в Java, поскольку прослушиватель кликов может быть запущен в любое время, и во время его выполнения x
может уже отсутствовать или его значение могло измениться. Java заставляет нас сделать эту переменную окончательной, чтобы фактически сделать ее неизменной.
Как только он станет неизменным, Java будет знать, что x
всегда будет 5
всякий раз, когда выполняется прослушиватель кликов. Эта система несовершенна, поскольку x
может указывать на список, который можно изменять, даже если ссылка на список такая же.
В Java нет механизма, позволяющего функции захватывать переменные, выходящие за пределы ее области, и реагировать на них. Функции Java не могут захватывать или закрывать свою среду.
Попробуем проделать то же самое в Котлине. Нам даже не нужен анонимный внутренний класс, поскольку в Kotlin есть функции первого класса:
var x = 5 view.setOnClickListener { println(x) }
Это совершенно справедливо в Котлине. Функции в Kotlin - это замыкания. Они могут отслеживать обновления в своей среде и отвечать на них.
При первом запуске прослушивателя кликов он напечатает 5
. Если мы затем изменим значение x
, скажем x = 9
и снова запустим прослушиватель щелчков, на этот раз он напечатает 9
.
Итак, что я могу сделать с этими закрытием?
Замыкания имеют много изящных вариантов использования. Каждый раз, когда вы хотите, чтобы бизнес-логика реагировала на какое-либо состояние в среде, вы можете использовать замыкания.
Предположим, у вас есть прослушиватель кликов на кнопке, который показывает пользователю диалог с кучей сообщений. Если у вас нет закрытий, вам придется инициализировать новый слушатель с новым списком сообщений каждый раз, когда сообщения меняются.
С помощью замыканий вы можете где-то хранить список сообщений и передавать ссылку на список в слушателе, как мы делали выше, и слушатель всегда будет показывать последний набор сообщений.
Замыкания также можно использовать для полной замены объектов. Это часто используется в функциональных языках, где вам может потребоваться некоторое поведение, подобное ООП, и язык не поддерживает их.
Давайте посмотрим на пример:
class Dog { private var weight: Int = 10 fun eat(food: Int) { weight += food } fun workout(intensity: Int) { weight -= intensity } }
У меня есть собака, которая набирает вес, когда мы ее кормим, и худеет, когда тренируется. Можем ли мы описать то же поведение с помощью замыканий?
fun main(args: Array<String>) { dog(Action.feed)(5) } val dog = { action: Action -> var weight: Int = 10 when (action) { Action.feed -> { food: Int -> weight += food; println(weight) } Action.workout -> { intensity: Int -> weight -= intensity; println(weight) } } } enum class Action { feed, workout }
Функция dog
принимает Action
и, в зависимости от действия, либо накормит собаку, либо заставит ее тренироваться. Когда мы вызываем dog(Action.feed)(5)
в функции main
, результат будет 15
. Функция dog
выполняет действие feed
и возвращает другую функцию, которая будет кормить собаку. Когда мы передаем значение 5
этой возвращаемой функции, она увеличивает вес собаки до 10 + 5 = 15
и распечатывает его.
Таким образом, комбинируя замыкания и функции высшего порядка, мы можем получить объекты без ООП.
Вы, вероятно, не захотите делать это в реальном коде, но интересно знать, что это возможно. Действительно, замыкания называют объектами бедняков.
Резюме
Функции высшего порядка во многих случаях позволяют нам инкапсулировать бизнес-логику лучше, чем ООП, и мы можем передавать их и обрабатывать как данные. Замыкания захватывают окружающую среду и помогают нам эффективно использовать функции высшего порядка.
В следующей части мы узнаем о функциональной обработке ошибок.
Если вам это понравилось, нажмите 👏 ниже. Я замечаю каждого и благодарен каждому из них.
Чтобы узнать больше о программировании, подпишитесь на меня, и вы будете получать уведомления, когда я буду писать новые сообщения.