Путать с пониманием преобразования flatMap/Map
Я действительно не понимаю карты и FlatMap. Что я не понимаю, так это то, что понимание - это последовательность вложенных вызовов map и flatMap. Следующий пример взят из Функционального программирования в Scala
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
f <- mkMatcher(pat)
g <- mkMatcher(pat2)
} yield f(s) && g(s)
переводит на
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] =
mkMatcher(pat) flatMap (f =>
mkMatcher(pat2) map (g => f(s) && g(s)))
Метод mkMatcher определяется следующим образом:
def mkMatcher(pat:String):Option[String => Boolean] =
pattern(pat) map (p => (s:String) => p.matcher(s).matches)
И шаблонный метод выглядит следующим образом:
import java.util.regex._
def pattern(s:String):Option[Pattern] =
try {
Some(Pattern.compile(s))
}catch{
case e: PatternSyntaxException => None
}
Было бы здорово, если бы кто-то смог пролить свет на обоснование использования map и flatMap здесь.
5 ответов
TL;DR перейти непосредственно к последнему примеру
Я попробую и подведу итоги
Определения
for
понимание - это сочетание синтаксиса flatMap
а также map
таким образом, что легко читать и рассуждать о.
Давайте немного упростим ситуацию и предположим, что каждый class
что обеспечивает оба вышеупомянутых метода можно назвать monad
и мы будем использовать символ M[A]
означать monad
с внутренним типом A
,
Примеры
Некоторые часто встречающиеся монады
List[String]
гдеM[_]: List[_]
A: String
Option[Int]
гдеM[_]: Option[_]
A: Int
Future[String => Boolean]
гдеM[_]: Future[_]
A: String => Boolean
карта и плоская карта
Определено в общей монаде M[A]
/* applies a transformation of the monad "content" mantaining the
* monad "external shape"
* i.e. a List remains a List and an Option remains an Option
* but the inner type changes
*/
def map(f: A => B): M[B]
/* applies a transformation of the monad "content" by composing
* this monad with an operation resulting in another monad instance
* of the same type
*/
def flatMap(f: A => M[B]): M[B]
например
val list = List("neo", "smith", "trinity")
//converts each character of the string to its corresponding code
val f: String => List[Int] = s => s.map(_.toInt).toList
list map f
>> List(List(110, 101, 111), List(115, 109, 105, 116, 104), List(116, 114, 105, 110, 105, 116, 121))
list flatMap f
>> List(110, 101, 111, 115, 109, 105, 116, 104, 116, 114, 105, 110, 105, 116, 121)
для выражения
Каждая строка в выражении, используя
<-
символ переводится вflatMap
вызов, за исключением последней строки, которая переводится в заключительнуюmap
вызов, где "связанный символ" слева передается в качестве параметра функции аргумента (то, что мы ранее называлиf: A => M[B]
):// The following ... for { bound <- list out <- f(bound) } yield out // ... is translated by the Scala compiler as ... list.flatMap { bound => f(bound).map { out => out } } // ... which can be simplified as ... list.flatMap { bound => f(bound) } // ... which is just another way of writing: list flatMap f
For-выражение только с одним
<-
превращается вmap
вызов с выражением, переданным в качестве аргумента:// The following ... for { bound <- list } yield f(bound) // ... is translated by the Scala compiler as ... list.map { bound => f(bound) } // ... which is just another way of writing: list map f
Теперь к делу
Как видите, map
операция сохраняет "форму" оригинала monad
то же самое происходит и для yield
выражение: List
остается List
с содержанием, преобразованным операцией в yield
,
С другой стороны, каждая обязательная строка в for
это просто композиция из последовательных monads
который должен быть "сплющен", чтобы сохранить единую "внешнюю форму".
Предположим на мгновение, что каждое внутреннее связывание было переведено в map
звоните, но правая рука осталась прежней A => M[B]
функция, вы бы в конечном итоге M[M[B]]
для каждой строки в понимании.
Цель всего for
Синтаксис состоит в том, чтобы легко "сгладить" конкатенацию последовательных монадических операций (то есть операций, которые "поднимают" значение в "монадической форме"): A => M[B]
), с добавлением финала map
операция, которая, возможно, выполняет заключительное преобразование.
Я надеюсь, что это объясняет логику выбора перевода, который применяется механически, то есть: n
flatMap
вложенные вызовы, заключенные одним map
вызов.
Придуманный иллюстративный пример
Предназначен для демонстрации выразительности for
синтаксис
case class Customer(value: Int)
case class Consultant(portfolio: List[Customer])
case class Branch(consultants: List[Consultant])
case class Company(branches: List[Branch])
def getCompanyValue(company: Company): Int = {
val valuesList = for {
branch <- company.branches
consultant <- branch.consultants
customer <- consultant.portfolio
} yield (customer.value)
valueList reduce (_ + _)
}
Можете ли вы угадать тип valuesList
?
Как уже говорилось, форма monad
поддерживается через понимание, поэтому мы начнем с List
в company.branches
и должен заканчиваться List
,
Вместо этого внутренний тип изменяется и определяется yield
выражение: которое customer.value: Int
valueList
должен быть List[Int]
Я не мега-разум Скала, поэтому не стесняйтесь поправлять меня, но вот как я объясняю flatMap/map/for-comprehension
сага про себя!
Чтобы понять for comprehension
и это перевод на scala's map / flatMap
мы должны сделать небольшие шаги и понять составляющие части - map
а также flatMap
, Но не scala's flatMap
просто map
с flatten
Вы спрашиваете себя! если так, то почему многим разработчикам так трудно понять это или for-comprehension / flatMap / map
, Ну, если вы просто посмотрите на скалы map
а также flatMap
подпись, которую вы видите, они возвращают один и тот же тип возврата M[B]
и они работают на одном входном аргументе A
(по крайней мере, первая часть функции, которую они принимают), если это так, что имеет значение?
Наш план
- Понять скалы
map
, - Понять скалы
flatMap
, - Понять скалы
for comprehension
.`
Карта Скалы
подпись карты Scala:
map[B](f: (A) => B): M[B]
Но когда мы смотрим на эту подпись, нам не хватает большой части, и это - где это A
происходит от? наш контейнер имеет тип A
поэтому важно взглянуть на эту функцию в контексте контейнера - M[A]
, Наш контейнер может быть List
предметов типа A
и наш map
Функция принимает функцию, которая преобразует каждый элемент типа A
печатать B
затем возвращает контейнер типа B
(или же M[B]
)
Напишем подпись карты с учетом контейнера:
M[A]: // We are in M[A] context.
map[B](f: (A) => B): M[B] // map takes a function which knows to transform A to B and then it bundles them in M[B]
Обратите внимание на чрезвычайно важный факт о карте - она автоматически упаковывается в выходной контейнер M[B]
Вы не можете это контролировать. Давайте снова подчеркнем это:
map
выбирает выходной контейнер для нас, и он будет тем же контейнером, что и источник, над которым мы работаемM[A]
контейнер мы получаем то же самоеM
контейнер только дляB
M[B]
и ничего больше!map
делает эту контейнеризацию для нас, мы просто даем отображение изA
вB
и он положил бы его в коробкуM[B]
положит его в коробку для нас!
Вы видите, что вы не указали, как containerize
элемент, который вы только что указали, как преобразовать внутренние элементы. И так как у нас есть тот же контейнер M
для обоих M[A]
а также M[B]
это означает M[B]
это тот же контейнер, то есть, если у вас есть List[A]
тогда у вас будет List[B]
и что более важно map
делает это для вас!
Теперь, когда мы имели дело с map
давайте перейдем к flatMap
,
Плоская карта Скалы
Давайте посмотрим его подпись:
flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]
Вы видите большую разницу от карты к flatMap
в flatMap мы предоставляем ему функцию, которая не просто конвертирует из A to B
но также контейнеризует его в M[B]
,
почему нас волнует, кто занимается контейнеризацией?
Так почему же мы так заботимся о функции ввода для map / flatMap, которая делает контейнеризацию M[B]
или сама карта делает для нас контейнеризацию?
Вы видите в контексте for comprehension
происходит многократное преобразование элемента, представленного в for
поэтому мы даем следующему работнику нашей сборочной линии возможность определить упаковку. представьте, что у нас есть сборочная линия, каждый работник что-то делает с продуктом, и только последний работник упаковывает его в контейнер! Добро пожаловать в flatMap
это его цель, в map
каждый рабочий по окончании работы с предметом также упаковывает его, поэтому вы получаете контейнеры над контейнерами.
Могучий для понимания
Теперь давайте посмотрим на ваше понимание, учитывая то, что мы сказали выше:
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
f <- mkMatcher(pat)
g <- mkMatcher(pat2)
} yield f(s) && g(s)
Что мы получили здесь:
mkMatcher
возвращаетcontainer
контейнер содержит функцию:String => Boolean
- Правила, если у нас есть несколько
<-
они переводят наflatMap
кроме последнего. - Как
f <- mkMatcher(pat)
первый вsequence
(считатьassembly line
) все, чего мы хотим от этого - это взятьf
и передать его следующему работнику на сборочной линии, мы даем возможность следующему работнику на нашей сборочной линии (следующей функции) определить, какой будет упаковка для нашего изделия, поэтому последняя функцияmap
, Последний
g <- mkMatcher(pat2)
буду использоватьmap
это потому что его последний на конвейере! так что он может просто сделать последнюю операцию сmap( g =>
что да! вытаскиваетg
и используетf
который уже вытащен из контейнераflatMap
поэтому мы заканчиваем с первым:mkMatcher (pat) flatMap (f // вытащить функцию f передать элемент следующему работнику сборочной линии (вы видите, что он имеет доступ к
f
и не упаковывать его обратно, я имею в виду, пусть карта определяет упаковку, пусть следующий рабочий сборочной линии определит контейнер. mkMatcher(pat2) map (g => f(s) ...)) // так как это последняя функция в сборочной линии, мы собираемся использовать map и вытащить g из контейнера и обратно в упаковку, ееmap
и эта упаковка поднимется и станет нашей упаковкой или контейнером, ага!
Логическое обоснование состоит в том, чтобы связать монадические операции, что обеспечивает надлежащую обработку ошибок "быстро сбой".
Это на самом деле довольно просто. mkMatcher
метод возвращает Option
(которая является монадой). Результат mkMatcher
монадическая операция, является либо None
или Some(x)
,
Применяя map
или же flatMap
функция к None
всегда возвращает None
- функция передана в качестве параметра map
а также flatMap
не оценивается.
Следовательно, в вашем примере, если mkMatcher(pat)
возвращает None, примененная к нему flatMap вернет None
(вторая монадическая операция mkMatcher(pat2)
исполняться не будет) и финал map
снова вернет None
, Другими словами, если какая-либо из операций для понимания возвращает None, у вас возникает поведение быстрого сбоя, а остальные операции не выполняются.
Это монадный стиль обработки ошибок. В императивном стиле используются исключения, которые в основном являются переходами (к предложению catch)
Последнее замечание: patterns
функция является типичным способом "перевода" обработки ошибок императивного стиля (try
...catch
) к обработке ошибок монадического стиля с использованием Option
Это может быть продано как:
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
f <- mkMatcher(pat) // for every element from this [list, array,tuple]
g <- mkMatcher(pat2) // iterate through every iteration of pat
} yield f(s) && g(s)
Запустите это для лучшего представления о том, как его расширили
def match items(pat:List[Int] ,pat2:List[Char]):Unit = for {
f <- pat
g <- pat2
} println(f +"->"+g)
bothMatch( (1 to 9).toList, ('a' to 'i').toList)
результаты:
1 -> a
1 -> b
1 -> c
...
2 -> a
2 -> b
...
Это похоже на flatMap
- цикл по каждому элементу в pat
и элемент foreach map
это к каждому элементу в pat2
Первый, mkMatcher
возвращает функцию, чья подпись String => Boolean
это обычная процедура Java, которая просто запустить Pattern.compile(string)
, как показано в pattern
функция. Затем посмотрите на эту строку
pattern(pat) map (p => (s:String) => p.matcher(s).matches)
map
функция применяется к результату pattern
, который Option[Pattern]
, Итак p
в p => xxx
это просто шаблон, который вы скомпилировали. Итак, учитывая шаблон p
, новая функция построена, которая принимает строку s
и проверьте, если s
соответствует шаблону.
(s: String) => p.matcher(s).matches
Обратите внимание p
Переменная ограничена скомпилированным шаблоном. Теперь понятно, что как функция с подписью String => Boolean
построен mkMatcher
,
Далее давайте проверим bothMatch
функция, которая основана на mkMatcher
, Чтобы показать как bothMathch
работает, мы сначала посмотрим на эту часть:
mkMatcher(pat2) map (g => f(s) && g(s))
Так как мы получили функцию с подписью String => Boolean
от mkMatcher
, который g
в данном контексте, g(s)
эквивалентно Pattern.compile(pat2).macher(s).matches
, который возвращает, если строка соответствует шаблону pat2
, Так как насчет f(s)
это так же, как g(s)
Единственное отличие состоит в том, что первый вызов mkMatcher
использования flatMap
, вместо map
, Зачем? Так как mkMatcher(pat2) map (g => ....)
возвращается Option[Boolean]
, вы получите вложенный результат Option[Option[Boolean]]
если вы используете map
для обоих звонков, это не то, что вы хотите.