Более подробный взгляд на операторы .map и .flatMap

С тех пор, как я начал программировать на Swift, я не мог не использовать такие функции, как map, reduce, filter и т. Д. При выполнении всевозможных операций с массивами. В них есть что-то настолько приятное, что я никогда не упущу возможности превратить мою обычную процедуру обработки массивов в цепочку этих изящных функциональных операторов. Но что, если я скажу вам, что вы можете расширить использование этой концепции за пределы только типов коллекций?

Возможно, для вас это не большая новость, но вы можете использовать оператор map с типом Optional.

Ничто не иллюстрирует проблему кодирования лучше, чем старый добрый «Hello world». Вот функция, которая принимает строку и ставит перед ней префикс «Hello»:

Но что произойдет, если мы захотим сделать как ввод, так и вывод опциональными? Это даст нам несколько дополнительных шагов с необходимостью отфильтровать nil:

Оператор guard let - это обычный способ разворачивать необязательные параметры, но обратите внимание, как функция стала намного длиннее только потому, что мы решили их поддержать. Есть способ написать это попроще? Да, есть. Взгляните на это:

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

Используемый нами оператор называется map, как и Array<T>.map. Несмотря на то, что эти два типа данных принципиально различаются (один является enum, а другой - типом коллекции), операторы map, которые они имеют, на самом деле следуют одной и той же логике.

И Array<T>, и Optional<T> являются универсальными конструкциями, содержащими внутренний тип. Для каждого типа, подобного этому, map - это оператор, который преобразует упакованное значение T, хранящееся внутри объекта упаковки типа Type<T>, в значение типа U и производит упаковку объект типа Type<U> с преобразованным значением внутри. Вот как выглядит подпись этого оператора:

map<U>(f: (T) -> U) -> Type<U>

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

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

Вернемся к предыдущему примеру. Может случиться так, что один из промежуточных результатов цепочки сопоставления также вернет необязательный объект. Давайте создадим функцию с именем helloRoot, которая вычисляет квадратный корень из Double и распечатывает строку «Квадратный корень равен \ (значению)», но вместо прямого использования встроенного метода sqrt давайте превратим его в функцию, которая возвращает nil для отрицательный ввод, чтобы мы не получили nan:

И тогда фактическая функция helloRoot выглядит так:

Этот набор операторов карты выглядит очень эстетично. Легко читать и видеть, что происходит в каждой строке. К сожалению, конечный результат выглядит не очень хорошо. Например, если мы передадим 25 в качестве входных данных, он вернет “Square root equals Optional(5.0)”, а если мы передадим отрицательный параметр, он напечатает “Square root equals nil”. Технически правильно, но визуально не радует. Если бы только был способ убрать мусор с вывода и вообще ничего не печатать, если печатать нечего.

Мы столкнулись с проблемой, когда один из операторов карты выдает Optional<Optional<Double>>. Это не должно вызывать удивления, поскольку метод root возвращает Double?.

Прежде чем найти способ исправить это, давайте взглянем на тривиальное решение с оператором guard/else, который разворачивает значения:

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

К счастью, есть способ избавиться от вложенности, которую создают map операторы. Если у вас есть опыт работы с функциями сопоставления типа Array, вы, возможно, уже заметили, что я говорю о flatMap. Он работает аналогично map, за исключением того, что ожидает, что функция преобразования вернет значение своего собственного типа Type<U>, но, в отличие от map, не перевернет его на другой уровень Type<Type<U>>, а оставит как есть. Следовательно, это своего рода «выравнивание» вложенных значений. Вот его подпись:

flatMap<U>(f: (T) -> Type<U>) -> Type<U>

Теперь вернемся к helloRoot с двумя функциями сопоставления и внесем небольшое изменение в строку 3:

Вуаля! Мы избавились от мусора на выходе. Если мы передадим 25 в качестве ввода, он напечатает “Square root equals 5.0”, а если мы передадим ошибочный ввод, он вернет nil.

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

  1. map используется для преобразования значения необязательного параметра в другое значение, предположительно также другого типа. Если внутри необязательного есть значение, оно пройдет через всю цепочку преобразований. В противном случае он будет продолжать возвращать nil. Простой.
  2. map используется для преобразования значения необязательного значения в другое необязательное. Поведение для nil не изменится, но для значения, отличного от nil, каждое другое преобразование добавит еще один уровень вложенности, например вы перейдете с .some(T) на .some(.some(U)), затем .some(.some(.some(V))) и т. д.
  3. flatMap используется для преобразования значения в другой необязательный параметр, как и в предыдущем примере. Однако каждый новый слой «сглаживается» до текущего уровня, поэтому в конце остается только один объект Optional. Подобно шару для боулинга, который падает с дорожки и продолжает катиться по желобу, как только значение становится nil, все последующие преобразования игнорируются, и оно остается nil до конца цепочки.

Надеюсь, это даст вам лучшее понимание того, как эти функции ведут себя и какие преимущества они вам приносят. Вы также можете заметить, что эта концепция довольно часто реализуется для различных типов данных. Например, очень популярный типResult<T, Error>, который широко используется в качестве контейнера для значений и ошибок, поступающих из сетевых запросов. Многие реализации, такие как, например, this, также поддерживают функции map и flatMap.

Но подождите, это еще не все. Взгляните еще раз на картинку, особенно на часть flatMap. Это поведение вам о чем-то напоминает? Выполнение идет вниз по цепочке необязательных значений, пока оно либо не завершится успешно, либо не достигнет nil. Если задуматься, Optional Chaining работает точно так же. Итак, должен быть способ разработать нашу собственную реализацию Optional Chaining, используя только операторы map и flatMap.

Предположим, что вместо применения функций преобразования мы хотим получить доступ к определенным свойствам. Начиная с Swift версии 4, мы можем использовать синтаксис KeyPath для доступа к свойствам, передав их имя в качестве параметра функции. Затем мы будем использовать flatMap для чтения необязательных свойств и map для доступа к необязательным. Чтобы имитировать исходный синтаксис, мы напишем расширение и воспользуемся оператором, который вам больше всего нравится, например ~>. Расширение выглядит так:

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

Попробуйте! Этот новый оператор может использоваться как альтернатива встроенному синтаксису цепочки. Вот так:

Практически никакой разницы, их можно использовать как взаимозаменяемые!

Как видите, Optional Chaining - это не волшебство, а просто синтаксический сахар над механизмом, который вы можете реализовать самостоятельно, используя возможности функций более высокого порядка. Теперь вы знаете, как использовать map и flatMap в своих интересах для написания более декларативного кода, и готовы заняться функциональными парадигмами, такими как RxSwift.

Не забывайте, что Swift имеет открытый исходный код! Если вы хотите узнать больше, вы можете сами изучить исходный код типа Optional.

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