В последнем посте мы узнали о неизменяемости и параллелизме. В этом разделе мы рассмотрим функции высшего порядка и замыкания.

Если вы не читали часть 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 и распечатывает его.

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

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

Резюме

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

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

Если вам это понравилось, нажмите 👏 ниже. Я замечаю каждого и благодарен каждому из них.

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