Цепь нулевого безопасного метода с опциональным

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

Этот вопрос связан с Guava Необязательный тип, когда преобразование возвращает другой Необязательный, который задает практически тот же вопрос, но для другого варианта использования, который, я думаю, может не быть предполагаемое использование Optional (ошибки обработки).

Рассмотрим метод Optional<Book> findBook(String id). findBook(id).transform(Book.getName) работает так, как ожидалось. Если нет книги, мы получаем Absent<String>, если есть найденная книга, мы получаем Present<String>.

В общем случае, когда промежуточные методы могут возвращать null/absent(), кажется, нет элегантного способа цепной связи. Например, предположим, что Book имеет метод Optional<Publisher> getPublisher(), и мы хотели бы получить все книги, изданные издателем книги. Естественный синтаксис будет выглядеть как findBook(id).transform(Book.getPublisher).transform(Publisher.getPublishedBooks), однако это не сработает, потому что вызов transform(Publisher.getPublishedBooks) действительно вернет Optional<Optional<Publisher>>.

Кажется довольно разумным иметь transform() -подобный метод на Optional, который будет принимать функцию, которая возвращает Optional. Он будет действовать точно так же, как и текущая реализация, за исключением того, что он просто не будет обертывать результат функции в Необязательном. Реализация (для Present) может читать:

public abstract <V> Optional<V> optionalTransform(Function<? super T, Optional<V>> function) {
    return function.apply(reference);
}

Реализация для Absent не изменяется от transform:

public abstract <V> Optional<V> optionalTransform(Function<? super T, Optional<V>> function) {
    checkNotNull(function);
    return Optional.absent();
}

Было бы неплохо, если бы был способ обработать методы, возвращающие null, а не Optional для работы с устаревшими объектами. Такой метод будет похож на transform, а просто вызовет Optional.fromNullable на результат функции.

Мне любопытно, если кто-то еще столкнулся с этим досадой и нашел хорошие обходные пути (которые не связаны с написанием собственного класса Optional). Я также хотел бы услышать от команды Гуавы или указать на дискуссии, связанные с проблемой (я не нашел их в поиске).

Ответ 1

Вы ищете некоторую Monad, но Guava Необязательный (как противоположно, например, Scala Option) - это просто Functor.

Какого черта Functor?!

Functor и Monad - это своего рода коробка, контекст, который обертывает некоторую ценность. Functor, содержащий некоторое значение типа A, знает, как применить функцию A = > B и вернуть результат обратно в Functor. Например: получить что-то из Необязательного, преобразовать и обернуть обратно в Необязательный. В языках функционального программирования такой метод часто называют "map".

Mona.. что?

Monad - почти то же самое, что и Functor, за исключением того, что он потребляет функцию, возвращающую значение, завернутую в Monad (A = > Monad, например Int = > Необязательно). Этот волшебный метод Монады часто называют "flatMap".

Здесь вы можете найти действительно удивительные объяснения для основных терминов FP: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

Фанкорторы и Монады идут!

Необязательно с Java 8 можно классифицировать как Functor (http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#map-java.util.function.Function-) и Monad (<а2 > ).

Nice mon (ad) olog, Marcin, но как я могу решить свою конкретную проблему?

В настоящее время я работаю над проектом, который использует Java 6, и вчера я пишу некоторый вспомогательный класс, называемый "Optionals", который сэкономил мне много времени.

Он предоставляет некоторый вспомогательный метод, который позволяет мне повернуть Необязательный в Monads (flatMap).

Вот код: https://gist.github.com/mkubala/046ae20946411f80ac52

Поскольку моя кодовая база проекта по-прежнему использует nulls как возвращаемое значение, я ввел Optionals.lift(Function), который можно использовать для переноса результатов в Необязательный.

Зачем поднимать результат в Необязательный? Чтобы избежать ситуации, когда функция, переданная в преобразование, может возвращать значение null, а целое выражение возвращает "present of null" (что, кстати, невозможно с Guava Необязательно, из-за этого постусловия → см. Строку # 71 https://code.google.com/p/guava-libraries/source/browse/guava/src/com/google/common/base/Present.java?r=0823847e96b1d082e94f06327cf218e418fe2228#71).

Несколько примеров

Предположим, что findEntity() возвращает необязательный элемент и Entity.getDecimalField(..) может возвращать BigDecimal или null:

Optional<BigDecimal> maybeDecimalValue = Optionals.flatMap(
    findEntity(),
    new Function<Entity, Optional<BigDecimal>> () {
        @Override 
        public Optional<BigDecimal> apply(Entity input) {
            return Optional.fromNullable(input.getDecimalField(..));
        }
    }
);

Еще один пример, предполагающий, что у меня уже есть функция, которая извлекает десятичные значения из Entities и может возвращать значения null:

Function<Entity, Decimal> extractDecimal = .. // extracts decimal value or null
Optional<BigDecimal> maybeDecimalValue = Optionals.flatMap(
    findEntity(),
    Optionals.lift(extractDecimal)
);

И последнее, но не менее важное - пример использования:

Optional<Publisher> maybePublisher = Optionals.flatMap(findBook(id), Optionals.lift(Book.getPublisher));

// Assuming that getPublishedBooks may return null..
Optional<List<Book>> maybePublishedBooks = Optionals.flatMap(maybePublisher, Optionals.lift(Publisher.getPublishedBooks));

// ..or simpler, in case when getPublishedBooks never returns null
Optional<List<Book>> maybePublishedBooks2 = maybePublisher.transform(Publisher.getPublishedBooks);

// as a one-liner:
Optionals.flatMap(maybePublisher, Optionals.lift(Publisher.getPublishedBooks)).transform(Publisher.getPublishedBooks);

Ответ 2

Вероятно, вы поняли это, но вы можете добавить .or(Optional.absent) после каждого преобразования, которое возвращает Optional (в вашем случае после .transform(Book.getPublisher), уменьшая Optional<Optional<T>> до Optional<T>:

Optional<List<Book>> publishedBooks = findBook(id).transform(Book.getPublisher).
        or(Optional.absent()).transform(Publisher.getPublishedBooks);

К сожалению, тип Optional.absent здесь не может быть выведен, поэтому код фактически становится:

Optional<List<Book>> publishedBooks = book.transform(Book.getPublisher).
        or(Optional.<Publisher> absent()).transform(Publisher.getPublishedBoooks);

Не слишком удобно, но, похоже, не существует другого способа.