Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Реализация пар Java: Составные данные

"Как видно, совершенство достигается не тогда, когда уже нечего прибавить, но когда уже ничего нельзя отнять." (c) Антуан де Сент-Экзюпери.

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

var pair = cons(a, b);

// для ссылочных типов аналогичные условия с equals()
a == car(pair); // true
b == cdr(pair); // true

Итак, пара — это соединение a и b, при этом a мы получаем через car, b — через cdr; a и b — это какие-то другие данные и они тоже могут быть парами. По сути, пара — это конструктор, селекторы и правила, которые определяют соотношения между конструктором, селекторами и данными.

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

public class Pairs {
    public static <T> PairType<T> cons(T a, T b) {
        return message -> switch (message) {
            case CAR -> a;
            case CDR -> b;
        };
    }

    public static <T> T car(PairType<T> pair) {
        return pair.apply(Messages.CAR);
    }

    public static <T> T cdr(PairType<T> pair) {
        return pair.apply(Messages.CDR);
    }
}

Классы PairType и Messages могли бы быть реализованы следующим образом:

import java.util.function.*;

enum Messages {
    CAR,
    CDR
}

interface PairType<T> extends Function<Messages, T> {};

Начнём с определения cons: это функция, которая принимает a и b, а внутри (и тут самое удивительное) содержит другую функцию, которая будет возвращена наружу. Внутри этой другой функции мы делаем switch, и если message равен 'car', возвращаем a, если 'cdr' — возвращаем b. Итак, в результате создания пары снаружи оказывается функция. Как же она работает? Очень просто: селекторы принимают пару и вызывают её как функцию, передавая в неё соответствующие сообщения. Если это селектор car, то передаётся сообщение 'CAR', если селектор cdr — то 'CDR'. А поскольку пара — это функция, которая принимает сообщение, то мы получаем то или иное значение в зависимости от переданного сообщения. Значения же сохраняются во внутренней функции за счёт замыкания.

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

public class Main {

    public static void main(String[] args) {
        System.out.println(car(Main::pair));
        System.out.println(cdr(Main::pair));
    }

    public static int pair(Messages message) {
        switch (message) {
            case CAR:
                return 4;
            case CDR:
                return 5;
            default:
                return 0;
        }
    }

    public static int car(Pair p) {
        return p.apply(Messages.CAR);
    }

    public static int cdr(Pair p) {
        return p.apply(Messages.CDR);
    }

}

interface Pair {
    int apply(Messages messages);
}

enum Messages {
    CAR,
    CDR
}

В этом примере мы можем представить, что пара — это функция, которая принимает сообщение и, если сообщение равно 'CAR', то возвращает 4, если 'CDR' — возвращает 5. Вызывая селектор car c аргументом pair, мы внутри вызываем эту функцию с сообщением 'CAR'. Интерфейс Pair введён для того, чтобы мы могли передавать одну функцию внутрь другой. Теперь car и cdr принимают на вход объекты, реализующие интерфейс Pair. Далее в метод этого объекта передаётся соответствующее сообщение.

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

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

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

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


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужна профессиональная подписка

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

Получить доступ
900
упражнения
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 120 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Изображение Тото

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