Pull to refresh

Comments 33

На КДПВ героиня из неплохого аниме Ergo Proxy.
У нее пули как-то криво летят
Мне кажется, это не пули, а motion blur от наведённого пистолета, но я уточню у нашей художницы.
И там был Прокси-Монада (не уверен что написал правильно)
Всё-таки если employee может быть null, то нам нужно изначально брать Optional.ofNullable(employee), потому как Optional.of(employee) как раз выдаст NullPointerException в случае employee == null.
Дока
Действительно. Спасибо, исправил.
Вот мне всегда нравятся такие «замечательные» примеры.

А что делать, если всё таки нужна обработка отсутствия данных о employee?

А логирование?
В Scala можно, например, коллектить в список Either[String, Error]
Ну я имел ввиду, что в статье об этом не рассказывают. А это ведь важно.

Обработка таких ситуаций вполне частая задача в промышленной разработке.
Очевидно, в этом случае придется проверять монаду на пустоту. Хотя да, согласен. В монаде из либы не хватает нормального механизма узнать на каком именно этапе у нас возникло бы npe.
Обработка исключительных ситуаций в стримах обсуждается, например, здесь.
Хочется это всё видеть в статье. Те примеры, что даны в этой статье достаточно просты и известны. Интересуют те самые детали, которых в статье нет.
Я вот лично сам не знаю как лучше. На мой взгляд проблема вообще надумана. Надо просто везде кидать unchecked exception, если что-то пошло не так. Восстановление после исключения в потоке не так часто требуется. Я запросил из базы данных 10 записей, на пятой произошёл connection timeout к базе. Не надо возвращать пользователю первые четыре записи, надо сказать, что ошибка произошла. А то он решит, что записей всего четыре.
я вас умоляю, оба варианта имеют право на жизнь, если у гугла где-то шард отвалится, вы хотели бы получить хотя б первую страницу поисковых результатов или только сообщение об ошибке?
если чо, с результатами можно и признак полноты вернуть
Но это же не туториал по использованию Optional, таких руководств тоже предостаточно. Задача статьи была продемонстрировать, как реализован паттерн монады в разных классах, естественно, я не стал погружаться в детали использования этих классов в разных юзкейсах.

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

Если вы имеете в виду логирование в функциональном коде, то его логичнее всего сделать либо внутри применяемых функций, либо (что ещё лучше) внутри монадического объекта, так как логирование само по себе побочный эффект.
Стоит заметить, что если у вас уже есть готовый API для, например, Company или Department, то вы не сможете добавлять методы, возвращающие Stream и будет иметь смысл использовать что-то типа:

List<String> streamEmployeeNames = companies.stream()
        .map(Company::getDepartments).flatMap(Collection::stream)
        .map(Department::getEmployees).flatMap(Collection::stream)
        .map(Employee::getName)
        .collect(toList());


На самом деле, мне кажется, что это выглядит даже опрятнее, чем метод автора, так как не требуется добавлять какие-то непонятные дополнительные методы.
У меня в либе есть шорткат для этого:

List<String> streamEmployeeNames = StreamEx.of(companies)
        .flatCollection(Company::getDepartments)
        .flatCollection(Department::getEmployees)
        .map(Employee::getName)
        .toList();
Совершенно верно, именно так и выглядел мой пример в своей первой версии. Но потом мне показалось, что эти дополнительные преобразования несколько затуманивают монадический паттерн, а задача была именно его здесь продемонстрировать. А так получается, что мы ушли от одного бойлерплейта, а пришли к другому: о)
На самом деле создавать методы, возвращающие Stream — это рекомендации лучших собаководов в Java-8. Если вы можете модифицировать ваш класс-коллекцию, добавьте метод, возвращающий Stream. И назовите его не getDepartmentsStream(), а просто departments(). Смотрите String.chars(), BufferedReader.lines(), Random.ints() и т. д. Если у вас есть старый метод, возвращающий какой-нибудь List или массив, и есть возможность прорефакторить, разумно удалить старый метод. Stream будет гибче и эффективнее.
Я бы сказал, что будет красивее и гибче. Насчет эффективнее можно поспорить)
Объясняю, почему это в целом эффективнее. Вот, скажем, у вас метод getDepartments() возвращает List. Поначалу вы внутри своего объекта хранили данные в ArrayList и потому метод выглядел примерно как return Collection.unmodifiableList(list);. Потом вы отрефакторили свой объект и решили, что удобнее втнутри хранить Map name -> department. Тогда в этом методе придётся написать return new ArrayList<>(map.values()). Пользователям же нашего метода далеко не всегда нужен именно List. В одном месте мы вызываем getDepartments().toArray(new Department[0]);, потому что дальше вызываем метод, которому требуется массив. В другом месте нам просто интересно, есть ли вообще хоть один элемент: if(!getDepartments().isEmpty()), в третьем нам нужны на самом деле списки сотрудников, как в примере автора статьи. Во всех этих случаях копирование значений мэпки в список оказывается излишней тратой времени и памяти. Если же мы будем возвращать Stream, мы можем рефакторить внутренности класса в весьма широких пределах и лишнего копирования не потребуется.
Я понял мысль. В целом согласен. Все зависит от дальнейшего использования.
А теперь задумаемся о смысле (жизни) к чему все это. Ключевое слово — динамическая композиция кода.

Т.н. «классический императивный стиль» соответствует статической композиции. Ветвистый код намертво прибит гвоздями к экрану изнутри, а его поведение определяется исключительно данными. Достоинства: простота и предопределенность. Недостаток — многописанина и сложности с более высоким уровнем абстракции: все должно быть записано явно.

В динамической композиции кода, из которой более чем полностью состоит это ваше функциональное программирование, последовательность действий выстраивается в процессе выполнения. Это позволяет абстрагироваться от конкретики и сконцентрироваться на результате действий (т.н. «декларативный стиль»), тогда как вся конкретика реализуется уже готовыми паттернами. Недостатки: сложность пошаговой отладки и чтения кода «с нуля».

Так что все эти модные акторы, монады, ФРП — способы динамической композиции кода.
Что-то я не понял вашу мысль. Где вы увидели динамическую композицию? Все действия (функции) известны на этапе компиляции, их «стыкуемость» друг с другом статически проверена с помощью системы типов — опять же на этапе компиляции. Да, если рассмотреть какой-нибудь функциональный, но всё же динамически типизированный JavaScript, аналогичные паттерны не будут поддерживаться статической системой типов, и там композиция кода действительно будет динамической, но это именно особенность системы типов языка, а не парадигмы ФП.

«Сложность пошаговой отладки» — странная формулировка применительно к ФП. В функциональном коде просто нечего «пошагово отлаживать», т.к. он не «пошаговый», а декларативный. Сложность чтения функционального кода — это миф и исключительно дело привычки.
Формально поведение кода всегда детерминировано, если вы про это. Однако для ФП связывание (композиция) может происходит в рантайме и может зависеть от сторонних факторов. В качестве примеров возьмите джавовский CompletableFuture или reactive programming. Сначала программа вычисляет результат композиции — некую комбинированную функцию, а затем данная функция применяется к набору данных и вычисляется конечный результат. Комбинированная функция состоит из множества блоков, но может быть скомпонована каждый раз по-разному — компоновка полностью контролируется рантаймом, отсюда и динамическая композиция. Для императивного подхода данный метод недоступен, поскольку вся композиция происходит на этапе компиляции. Не знаю, объяснил ли я мысль.
Ну представьте себе ветвистый вложенный-перевложенный if в процедурном коде. Там тоже есть множество блоков, конкретная последовательность выполнения которых на момент компиляции вам неизвестна и определяется состоянием. Чем это не «комбинированная функция из множества блоков» в ваших терминах? И она тоже компонуется каждый раз по-разному в рантайме. Да что там, в Java вы на этапе компиляции в большинстве случаев даже не знаете, какой именно код будет фактически исполнен при вызове метода у объектной переменной, потому что все методы (кроме статических, финальных и приватных) являются виртуальными.
Сложность пошаговой отладки действительно имеется, но по факту это вопрос перепрошивки мозга. Нужна ли она, пошаговая отладка пайплайнов-то? Если у вас методы, используемые в map и filter покрыты тестами, то отлаживать весь пайплайн не придётся. Здесь пошаговая отладка нужна даже меньше, чем в SQL-запросах (в которых без неё тоже как-то живут).

Насчёт чтения кода не соглашусь. Конечно, можно переусердствовать с функциональщиной, но корявый код можно написать в рамках любой концепции. Нормальный функциональный код читается легче, чем императивный, потому что читающий видит более высокие абстракции и не отвлекается на ненужные детали.
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { }


Подсветка синтаксиса хромает. Токен «extends»
Это явно не к автору поста:)
Sign up to leave a comment.