TL; DR перейти непосредственно к последнему примеру
Я попробую и резюмирую.
Определения
for
Понимание является синтаксис ярлыка , чтобы объединить flatMap
и map
таким образом , который легко читать и рассуждать о.
Давайте немного упростим ситуацию и предположим, что каждый, class
который предоставляет оба вышеупомянутых метода, может называться a, monad
и мы будем использовать этот символ M[A]
для обозначения a monad
с внутренним типом A
.
Примеры
Некоторые часто встречающиеся монады включают:
List[String]
где
M[X] = List[X]
A = String
Option[Int]
где
Future[String => Boolean]
где
M[X] = Future[X]
A = (String => Boolean)
карта и flatMap
Определен в общей монаде M[A]
def map(f: A => B): M[B]
def flatMap(f: A => M[B]): M[B]
например
val list = List("neo", "smith", "trinity")
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]
):
for {
bound <- list
out <- f(bound)
} yield out
list.flatMap { bound =>
f(bound).map { out =>
out
}
}
list.flatMap { bound =>
f(bound)
}
list flatMap f
For-выражение только с одним <-
преобразуется в map
вызов с выражением, переданным в качестве аргумента:
for {
bound <- list
} yield f(bound)
list.map { bound =>
f(bound)
}
list map f
Теперь к делу
Как видите, map
операция сохраняет «форму» оригинала monad
, то же самое происходит и с yield
выражением: a List
остается List
с содержимым, преобразованным операцией в yield
.
С другой стороны, каждая линия привязки в шаблоне for
- это просто последовательность monads
, которую необходимо «сплющить», чтобы сохранить единую «внешнюю форму».
Предположим на мгновение, что каждая внутренняя привязка была преобразована в map
вызов, но правая часть была той же самой A => M[B]
функцией, в результате вы получите a 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)
valuesList reduce (_ + _)
}
Вы можете угадать тип valuesList
?
Как уже было сказано, форма monad
поддерживается посредством понимания, поэтому мы начинаем с List
in company.branches
и должны заканчиваться на List
.
Вместо этого внутренний тип изменяется и определяется yield
выражением:customer.value: Int
valueList
должен быть List[Int]
Lists
. Если выmap
дважды выполняете функциюA => List[B]
(что является одной из важнейших монадических операций) над некоторым значением, вы получите List [List [B]] (мы считаем само собой разумеющимся, что типы совпадают). Внутренний цикл для понимания объединяет эти функции с помощью соответствующейflatMap
операции, «сглаживая» форму List [List [B]] в простой List [B] ... Надеюсь, это ясноyield
предложение iscustomer.value
, тип которогоInt
, поэтому всеfor comprehension
оценивается как aList[Int]
.Я не scala mega mind, так что не стесняйтесь поправлять меня, но вот как я объясняю
flatMap/map/for-comprehension
сагу себе!Чтобы понять
for comprehension
и это перевести,scala's map / flatMap
мы должны делать небольшие шаги и понимать составные части -map
иflatMap
. Но неscala's flatMap
толькоmap
сflatten
вами спросите себя! если да, то почему так многим разработчикам так трудно понять это или понятьfor-comprehension / flatMap / map
. Что ж, если вы просто посмотрите на scalamap
иflatMap
подпись, вы увидите, что они возвращают один и тот же тип возвратаM[B]
и работают с одним и тем же входным аргументомA
(по крайней мере, первая часть функции, которую они принимают), если это так, в чем разница?Наш план
map
.flatMap
.for comprehension
Разберитесь вКарта Scala
подпись карты 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
.Плоская карта Scala
Посмотрим его подпись:
flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]
Вы видите большую разницу между map и
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
, и не упаковывать его обратно, я имею в виду, пусть карта определяет упаковку, пусть следующий рабочий сборочной линии определяет container. mkMatcher (pat2) map (g => f (s) ...)) // поскольку это последняя функция в сборочной линии, мы собираемся использовать карту и вытащить g из контейнера и обратно в упаковку , егоmap
и эта упаковка будет дросселировать полностью и быть нашей упаковкой или нашим контейнером, да!источник
Обоснование состоит в том, чтобы объединить монадические операции в цепочку, что дает в качестве преимущества надлежащую «быструю обработку ошибок».
На самом деле это довольно просто.
mkMatcher
Метод возвращаетOption
(который является Монада). РезультатомmkMatcher
монадической операции является либо a,None
либо aSome(x)
.Применение функции
map
илиflatMap
к aNone
всегда возвращаетNone
- функцию, переданную в качестве параметраmap
иflatMap
не оцениваемую.Следовательно, в вашем примере, если
mkMatcher(pat)
возвращается None, примененная к нему flatMap вернетNone
(вторая монадическая операцияmkMatcher(pat2)
не будет выполнена), а финалmap
снова вернетNone
. Другими словами, если какая-либо из операций для понимания вернет None, это означает, что вы быстро отказываетесь, а остальные операции не выполняются.Это монадический стиль обработки ошибок. В императивном стиле используются исключения, которые в основном представляют собой переходы (к предложению catch).
Последнее замечание:
patterns
функция является типичным способом "перевода" обработки ошибок императивного стиля (try
...catch
) в обработку ошибок монадического стиля с использованиемOption
источник
flatMap
(а неmap
) используется для «сцепления» первого и второго вызоваmkMatcher
, но почемуmap
(а неflatMap
) используется «сцепление» второгоmkMatcher
иyields
блока?flatMap
ожидает, что вы передадите функцию, возвращающую результат, «завернутый» / поднятый в монаду, в то время как самmap
сделает упаковку / подъем. Во время цепочки вызовов операцийfor comprehension
вам необходимо,flatmap
чтобы функции, переданные в качестве параметра, могли возвращатьсяNone
(вы не можете поднять значение до None). Предполагается, что последний вызов операции, тот, которыйyield
находится в, будет запущен и вернет значение; a,map
чтобы связать эту последнюю операцию, достаточно и позволяет избежать необходимости поднимать результат функции в монаду.Это можно перевести как:
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
и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
создается новая функция, которая принимает Strings
и проверяет,s
соответствует ли шаблон.(s: String) => p.matcher(s).matches
Обратите внимание, что
p
переменная привязана к скомпилированному шаблону. Теперь ясно, как создается функция с подписью сString => Boolean
помощьюmkMatcher
.Далее давайте проверим
bothMatch
функцию, в основе которой лежитmkMatcher
. Чтобы показать, какbothMathch
работает, сначала посмотрим на эту часть:Поскольку у нас есть функция с сигнатурой
String => Boolean
frommkMatcher
, котораяg
в этом контекстеg(s)
эквивалентнаPattern.compile(pat2).macher(s).matches
, которая возвращает, если String s соответствует шаблонуpat2
. Так как насчетf(s)
того жеg(s)
, с той лишь разницей, что при первом вызовеmkMatcher
useflatMap
вместо «map
Почему»? ПосколькуmkMatcher(pat2) map (g => ....)
возвращаетсяOption[Boolean]
, вы получите вложенный результат,Option[Option[Boolean]]
если используетеmap
для обоих вызовов, это не то, что вы хотите.источник