Практическое введение в Monad Transformers, с подходом от проблемы к решению.
Итак, вы садитесь за свой стол, пьете кофе и готовитесь написать еще немного кода Scala. Функциональное программирование не так страшно, как вы думали, жизнь хороша, вы напрягаете мышцы и мозг и начинаете набрасывать ту новую функцию, которую вам нужно реализовать на этой неделе.
Это такой же день, как и многие другие: какие-то великолепные одинарные подсказки (ура, Scala!), Какие-то странные ошибки (о, нет, Scala!), Некоторые сожаления по поводу этих одинарных прокладок ... Затем вы сталкиваетесь со странной проблемой: ваше понимание не соответствует не компилировать. «Ничего страшного», - думаете вы, «позвольте мне проверить StackOverflow», как вы делаете каждый день. Как все мы делаем каждый день.
Только вот сегодня будет плохой день.
Сначала вы думали, что главный ответ - излишне умный. В большинстве случаев вы прокручиваете страницу вниз, находите более простое решение и без особого сожаления отклоняете решение математической теории категорий, монад и пони.
Но на этот раз все по-другому, на этот раз второй ответ похож на первый, третий и четвертый. В чем дело?
Монада. Трансформеры.
Даже название звучит устрашающе.
В чем снова была ваша проблема?
Все началось, когда вы набросали свою функцию
def findUserById(id: Long): Future[User] = ??? def findAddressByUser(user: User): Future[Address] = ???
Сначала это выглядело так элегантно: Future
представляет асинхронное вычисление и имеет метод flatMap
, что означает, что вы можете поместить его в for-complation. Блестяще!
def findAddressByUserId(id: Long): Future[Address] = for { user <- findUserById(id) address <- findAddressByUser(user) } yield address
Затем внезапное осознание: не все возможные идентификаторы соответствуют существующему пользователю. Что делать, если пользователя не удается найти? О, ну разве Option
не служит только цели?
def findUserById(id: Long): Future[Option[User]] = ???
и - теперь, когда вы думаете об этом - у некоторых пользователей даже нет адреса!
def findAddressByUser(user: User): Future[Option[Address]] = ???
Вернемся к работе ... о, ошибка компиляции?
def findAddressByUserId(id: Long): Future[Address] = for { user <- findUserById(id) address <- findAddressByUser(user) } yield address
Верно, теперь возвращаемый тип Future[Option[Address]]
def findAddressByUserId(id: Long): Future[Option[Address]] = for { user <- findUserById(id) address <- findAddressByUser(user) } yield address
Счастливы сейчас? Давай вернемся к ... подожди, снова ?! Что случилось сейчас?
error: type mismatch; found : Option[User] required: User address <- findAddressByUser(user)
Это не выглядит хорошо… Поразмыслив немного, вы понимаете, что <-
- это просто причудливый способ написания flatMap
и что если вы flatMap
над Future[Option[User]]
, вы получите работу с Option[User]
. Жаль, что тебе нужен User
…
Вы пробуете несколько альтернатив, но ни одна из них не выглядит хорошо. Лучшее, что вы можете придумать, выглядит так:
def findAddressByUserId(id: Long): Future[Option[Address]] = findUserById(id).flatMap { case Some(user) => findAddressByUser(user) case None => Future.successful(None) }
Ужасно или, по крайней мере, не так красиво, как раньше.
В идеале вам нужно что-то вроде
def findAddressByUserId(id: Long): Future[Option[Address]] = for { user <- userOption <- findUserById(id) address <- addressOption <- findAddressByUser(user) } yield address
Что-то вроде двойного flatMap
для извлечения как из Option
, так и из Future
. Но никто не упоминает ничего подобного в StackOverflow ...
Итак, в чем здесь настоящая проблема? Почему у нас просто не может быть superflatMap
, который работает на Future[Option[X]]
?
Уважаемый читатель, сделайте глубокий вдох: мы собираемся упомянуть кое-что теоретическое, но не отчаивайтесь. Вот все, что вам нужно знать, чтобы продолжить:
1.
Functor
- это структура с функциейmap
.
2.
Monad
- это структура с функциейflatMap
.
Вот и все. Я обещаю.
Некоторые базовые знания теории категорий помогают разгадывать загадку.
Если у вас есть два Functors
для A
и B
(т.е. вы знаете, как map
больше A
и B
), вы можете составить их вместе, ничего не зная. Это означает, что вы можете взять A[B[X]]
и получить Functor[A[B[X]]
, составив Functor[B]
и Functor[A]
.
Другими словами, если вы знаете, как отображать более A[X]
и более B[X]
, вы также знаете, как отображать более A[B[X]]
. Автоматически. Бесплатно.
Это неверно для Monad
: знание того, как flatMap
за A[X]
и B[X]
, не дает вам возможности волшебным образом получить flatMap
за A[B[X]]
.
Оказывается, это хорошо известный факт: Монады не складываются, по крайней мере, в общем.
Итак, да, Monads
может не создавать в целом, но вам нужен только flatMap
(и map
), который работает для Future[Option[A]]
.
Мы определенно можем это сделать: давайте напишем оболочку для Future[Option[A]]
, которая предоставляет свои собственные map
и flatMap
.
case class FutOpt[A](value: Future[Option[A]]) { def map[B](f: A => B): FutOpt[B] = FutOpt(value.map(optA => optA.map(f))) def flatMap[B](f: A => FutOpt[B]): FutOpt[B] = FutOpt(value.flatMap(opt => opt match { case Some(a) => f(a).value case None => Future.successful(None) })) }
Не плохо! Давайте попробуем!
def findAddressByUserId(id: Long): Future[Option[Address]] = (for { user <- FutOpt(findUserById(id)) address <- FutOpt(findAddressByUser(user)) } yield address).value
Оно работает! 🎉
Это здорово, если у вас есть Future[Option[A]]
, но что, если у вас, скажем, List[Option[A]]
? Может еще обертка? Давай попробуем:
case class ListOpt[A](value: List[Option[A]]) { def map[B](f: A => B): ListOpt[B] = ListOpt(value.map(optA => optA.map(f))) def flatMap[B](f: A => ListOpt[B]): ListOpt[B] = ListOpt(value.flatMap(opt => opt match { case Some(a) => f(a).value case None => List(None) })) }
Ммм, это похоже на FutOpt
, не так ли?
Если вы присмотритесь, то поймете, что нам не нужно знать ничего конкретного о «внешнем» Monad
(Future
и List
из предыдущих примеров). Пока мы можем map
и flatMap
справиться с этим, у нас все в порядке. С другой стороны, посмотрите, как мы деструктурировали Option
? Это некоторые конкретные знания о «внутреннем» Monad
(в данном случае Option
), которые нам необходимы.
Имея эту информацию в нашем кармане, мы можем написать общую структуру данных, которая «обертывает» любые Monad
M
вокруг Option
.
Вот шокирующая новость: мы только что случайно изобрели преобразователь монад, обычно называемый OptionT
! 😱
OptionT
имеет два параметра типа F
и A
, где F
- это обертка Monad
, а A
- это тип внутри Option
: другими словами, OptionT[F, A]
- это плоская версия F[Option[A]]
, которая имеет свои собственные map
и flatMap
.
OptionT[F, A]
- это плоская версия F [Option [A]], которая сама является монадой
Обратите внимание, что OptionT
также является монадой, поэтому мы можем использовать ее для понимания (в конце концов, в этом и был смысл).
Если вы используете библиотеки типа cats
, многие преобразователи монад (OptionT
, EitherT
,…) уже доступны.
Вернемся к нашему первоначальному примеру, на этот раз с котиками и трансформерами:
import cats.data.OptionT, cats.std.future._ def findAddressByUserId(id: Long): Future[Option[Address]] = (for { user <- OptionT(findUserById(id)) address <- OptionT(findAddressByUser(user)) } yield address).value
Оно работает!
Можем ли мы сделать это еще лучше? Вероятно: если мы обнаруживаем, что часто переносим упаковку, мы могли бы подумать об изменении этих методов, чтобы они возвращали OptionT[F, A]
напрямую. Давайте посмотрим
def findUserById(id: Long): OptionT[Future, User] = OptionT { ??? } def findAddressByUser(user: User): OptionT[Future, Address] = OptionT { ??? } def findAddressByUserId(id: Long): OptionT[Future, Address] = for { user <- findUserById(id) address <- findAddressByUser(user) } yield address
И это очень похоже на наш оригинальный пример. Когда нам нужен реальный Future[Option[Address]]
, мы можем просто вызвать .value
для получения результата.
В заключение несколько слов предостережения:
- Трансформаторы монад работают очень хорошо в некоторых распространенных случаях (например, в этом), но не заходите слишком далеко: Я не советую вкладывать более двух монад, так как это действительно очень сложно. В качестве трагического примера взгляните на этот README: https://github.com/djspiewak/emm.
- Monad Transformers не бесплатны с точки зрения распределения. При этом выполняется множество операций по упаковке / развертыванию, поэтому если производительность является проблемой, подумайте дважды и выполните несколько тестов.
- Поскольку они не являются стандартными для языка (доступно несколько библиотечных реализаций:
cats
,scalaz
и, возможно, другие), не раскрывайте их как общедоступный API. Позвоните.value
своим трансформаторам и покажите обычныйA[B[X]]
, который не навязывает вашим пользователям никакого самоуверенного выбора, а также позволяет вам менять реализации без внесения критических изменений.
Наконец, преобразователи монад - это всего лишь один из возможных способов работы со сложенными монадами. Это простой способ, если у вас есть простая проблема и вы не хотите сильно менять кодовую базу, но если вы готовы к большему скачку, вы можете взглянуть на Eff
.
Итак, напомним, преобразователи монад помогают нам иметь дело с вложенными монадами, предоставляя плоское представление двух вложенных монад, которое является самой монадой.
Надеюсь, я доказал, что они не так страшны, как звучат, и что вы могли придумать их сами (и, вероятно, в какой-то степени уже придумали).
У них есть несколько очень практичных вариантов использования, но не злоупотребляйте ими.
Если вы хотите узнать больше об этом, вот мой доклад на Scala Days 2017:
Удачного (функционального) программирования!
—
Если вы хотите работать там, где мы заботимся о качестве нашего рабочего процесса разработки, взгляните на https://buildo.io/careers