Смутно с пониманием преобразования flatMap/Map
Я действительно не понимаю понимания 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
}
Будет здорово, если кто-то может пролить свет на обоснование использования карты и flatMap здесь.
Ответы
Ответ 1
TL; DR перейти непосредственно к последнему примеру
Я попробую и резюме.
Определения
for
понимания является синтаксис ярлыка объединить flatMap
и map
таким образом, что легко читать и рассуждать о.
Давайте немного упростим ситуацию и предположим, что каждый class
который предоставляет оба вышеупомянутых метода, можно назвать monad
и мы будем использовать символ M[A]
для обозначения monad
с внутренним типом A
Примеры
Некоторые часто встречающиеся монады включают в себя:
-
List[String]
где -
M[X] = List[X]
-
A = String
-
Option[Int]
где -
Future[String => Boolean]
где -
M[X] = Future[X]
-
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)
valuesList reduce (_ + _)
}
Можете ли вы угадать тип valuesList
?
Как уже было сказано, форма monad
поддерживается через понимание, поэтому мы начинаем с List
в company.branches
и должны заканчиваться List
.
Вместо этого внутренний тип изменяется и определяется выражением yield
: customer.value: Int
valueList
должен быть List[Int]
Ответ 2
Я не мега-ум scala, поэтому не стесняйтесь поправлять меня, но вот как я объясняю сагу flatMap/map/for-comprehension
для себя!
Чтобы понять for comprehension
и его перевод на scala map / flatMap
, мы должны сделать небольшие шаги и понять составные части - map
и flatMap
. Но не scala flatMap
просто map
с flatten
вы спрашиваете себя! если да, то почему многим разработчикам так сложно понять его или for-comprehension / flatMap / map
. Ну, если вы просто посмотрите на scala map
и flatMap
подпись, то увидите, что они возвращают один и тот же тип возврата M[B]
, и они работают с одним и тем же аргументом ввода A
(по крайней мере, первая часть функции, которую они возьмите), если это так, что имеет значение?
Наш план
- Понять scala
map
.
- Понять scala
flatMap
.
- Понять scala
for comprehension
.
Scala map
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
Посмотрим на его подпись:
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
возвращает a 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)...))//поскольку это последняя функция на конвейере, мы собираемся использовать карту и вытащить g из контейнера и обратно в упаковку, ее map
, и эта упаковка будет полностью дросселировать и быть нашим пакетом или нашим контейнером, yah!
Ответ 3
Обоснование заключается в цепи монодических операций, которые обеспечивают как преимущество, правильную обработку ошибок "сбой".
На самом деле это довольно просто. Метод mkMatcher
возвращает Option
(который является Монадой).
Результатом mkMatcher
, монадической операции, является либо None
, либо Some(x)
.
Применение функции map
или flatMap
к None
всегда возвращает a None
- функция, переданная как параметр map
и flatMap
, не оценивается.
Следовательно, в вашем примере, если mkMatcher(pat)
возвращает значение None, то flatMap, примененный к нему, вернет значение None
(вторая монадическая операция mkMatcher(pat2)
не будет выполнена), а окончательный map
снова вернет None
.
Другими словами, если какая-либо из операций в понимании, возвращает None, у вас есть быстрое поведение, а остальные операции не выполняются.
Это монадический стиль обработки ошибок. В императивном стиле используются исключения, которые в основном являются переходами (в предложение catch)
Последнее замечание: функция patterns
является типичным способом "перевода" обработки ошибок поэтапного стиля (try
... catch
) в обработку ошибок монадического стиля с помощью Option
Ответ 4
Это может быть передано как:
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
Ответ 5
Во-первых, mkMatcher
возвращает функцию с сигнатурой String => Boolean
, которая представляет собой обычную java-процедуру, которая просто запускает Pattern.compile(string)
, как показано в функции pattern
.
Затем посмотрите на эту строку
pattern(pat) map (p => (s:String) => p.matcher(s).matches)
Функция map
применяется к результату pattern
, который равен Option[Pattern]
, поэтому p
in 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
, которая возвращает, если строка s соответствует шаблону pat2
. Итак, как насчет f(s)
, это так же, как g(s)
, единственное отличие состоит в том, что при первом вызове mkMatcher
используется flatMap
вместо map
, почему? Поскольку mkMatcher(pat2) map (g => ....)
возвращает Option[Boolean]
, вы получите вложенный результат Option[Option[Boolean]]
, если вы используете map
для обоих вызовов, это не то, что вы хотите.