Пожалуйста, используйте необязательно‹› по назначению!

1. Обзор

Во-первых, давайте посмотрим, откуда возникает потребность в Optional (и вообще в monads). Для этого мы начнем с аналогии и подумаем о процессе приготовления хлеба.

  • во-первых, мы будем делать муку, перемалывая пшеницу;
  • после этого мы будем делать хлеб, используя муку из предыдущего шага;

Мы можем выразить это в программных или математических терминах, используя функции:

flour = mill( wheat )
bread = bake( flour )

Или мы можем просто скомпоновать две функции следующим образом:

bread = bake( mill(wheat) )

2. Угловые случаи

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

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

В зависимости от используемого языка программирования и контекста это может иметь разные последствия — например, может быть выдано исключение, может быть возвращено значение null и т. д.

Предположим, что в нашем случае, если что-то пойдет не так с одним из процессов, возвращается null.

flour = mill( wheat )
if( flour != null ) {
   bread = bake( flour )
   if( bread != null ) {
     // .... do something()
   }
}

На данный момент мы больше не можем составлять методы из-за нулевых проверок.

3. Знакомство с монадой

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

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

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

Поэтому мы можем построить свой собственный объект-монаду или использовать один из существующих. В языках программирования определено множество монад с функциональными возможностями. В зависимости от языка нужная нам монада может называться как-то вроде Maybe‹›, Either‹› или Optional‹›.

4. map()и flatMap()

Давайте завернем типы возвращаемых функций в Необязательный‹› и попробуем вернуться к композиции функций:

Теперь у нас появится новая проблема: чтобы составить или связать функции, каждая из них должна получить в качестве параметра результат предыдущей.

Следовательно, функция bake() должна принимать тип Optional‹Flour› вместо необработанного типа Flour.

Но, поскольку функция bake упаковывает результат в Optional‹›, результат второго шага будет завернут дважды, результат третьего шага будет завернут 3 раза. раз.. и тд.

Решением этой проблемы является функция flatMap().Цель flatMap() — сохранить структуру плоский при преобразовании из одного типа в другой.

Другими словами, мы можем использовать следующие соглашения для Monad:

  • flatten :Monad‹Monad‹A›› Monad‹A›
  • карта: Монада‹A› Монада‹B›
  • flatMap :Monad‹A› Monad‹Monad‹B›› Monad‹B›

5. Функциональное программирование против ООП

Составление функций и методы объединения в цепочки

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

Но как нам это сделать? У нас есть два возможных решения для этого.

5.1. Ф.П. Путь (составление функций)

Во-первых, мы можем сделать так, чтобы функции получали и возвращали монады:

public Optional<Flour> mill(Optional<Wheat> wheat) {
   // ...
}

public Optional<Bread> bake(Optional<Flour> flour) {
   // ...
}

Это позволит нам скомпоновать функции в стиле функционального программирования:

Optional<Bread> fpBread = bake( mill( harvest() ) );

5.2. O.O.P. Путь (методы цепочки)

Java — это O.O.P. язык, сам необязательный объект предоставляет методы map и flatMap. Это означает, что нет необходимости передавать Необязательный объект в качестве параметра — на самом деле это считается запахом кода и анти-шаблоном (подробнее об этом в следующем разделе). ).

Поэтому мы должны использовать необработанные типы в качестве параметров метода:

public Optional<Flour> mill(Wheat wheat) {
   // ...
}

public Optional<Bread> bake(Flour flour) {
   // ...
}

И цепочка вызовов методов вместо составления функций с использованием Optional map() и flatMap():

Optional<Bread> oopBread = harvest()
      .flatMap(wheat -> mill(wheat))
      .flatMap(flour -> bake(flour));

6. Выводы. Какой хлеб лучше?

Когда Ф.П. функции были добавлены в O.O.P. таких языков, как Java, некоторые из них не использовались по назначению, что приводило к запахам кода и анти-шаблонам.

Придерживаясь примера с Java, некоторые разработчики и команды категорически против любого использования монады Optional‹›. Например, эта популярная статья под названием «Нет ничего лучше, чем необязательный тип».

Однако, если мы прочитаем статью и просмотрим фрагменты кода, мы увидим, что большинство проблем вызвано непреднамеренным использованием Optional.

На мой взгляд, Необязательно следует использовать как F.P. monad, а не как служебный класс Java для проверки null. Поэтому я нахожу все сравнение между ним и проверкой нуля немного неудачным.

Я считаю, что функция Optional‹› — отличное дополнение к языку, но она требует дисциплины и некоторого понимания концепции монад .

Эта статья была вдохновлена ​​выступлением Сезара Трона-Лозаи на DEVOX. Я настоятельно рекомендую проверить это для более подробного объяснения:

Спасибо!

Спасибо за прочтение статьи и, пожалуйста, дайте мне знать, что вы думаете! Любая обратная связь приветствуется.

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

Если вам нравится мой контент, подумайте о том, чтобы подписаться на список рассылки.

Наконец, если вы хотите стать участником Medium и поддержать мой блог, вот мой реферал.

Удачного кодирования!