Путать с преобразованием for-computing в flatMap / Map


87

Кажется, я действительно не понимаю Map и FlatMap. Чего я не понимаю, так это того, что for-complation представляет собой последовательность вложенных вызовов 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.

Ответы:


200

TL; DR перейти непосредственно к последнему примеру

Я попробую и резюмирую.

Определения

forПонимание является синтаксис ярлыка , чтобы объединить flatMapи mapтаким образом , который легко читать и рассуждать о.

Давайте немного упростим ситуацию и предположим, что каждый, classкоторый предоставляет оба вышеупомянутых метода, может называться a, monadи мы будем использовать этот символ M[A]для обозначения a monadс внутренним типом A.

Примеры

Некоторые часто встречающиеся монады включают:

  • List[String] где
    • M[X] = List[X]
    • A = String
  • Option[Int] где
    • M[X] = Option[X]
    • A = Int
  • Future[String => Boolean] где
    • M[X] = Future[X]
    • A = (String => Boolean)

карта и flatMap

Определен в общей монаде 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)

для выражения

  1. Каждая строка в выражении, использующем <-символ, преобразуется в 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
    
  2. 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выражением: 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поддерживается посредством понимания, поэтому мы начинаем с Listin company.branchesи должны заканчиваться на List.
Вместо этого внутренний тип изменяется и определяется yieldвыражением:customer.value: Int

valueList должен быть List[Int]


1
Слова «is the same as» относятся к метаязыку и должны быть удалены из блока кода.
день

3
Это должен прочитать каждый новичок в FP. Как этого добиться?
mert inan

1
@melston Приведем пример с Lists. Если вы mapдважды выполняете функцию A => List[B](что является одной из важнейших монадических операций) над некоторым значением, вы получите List [List [B]] (мы считаем само собой разумеющимся, что типы совпадают). Внутренний цикл для понимания объединяет эти функции с помощью соответствующей flatMapоперации, «сглаживая» форму List [List [B]] в простой List [B] ... Надеюсь, это ясно
pagoda_5b

1
читать твой ответ было просто потрясающе. Я бы хотел, чтобы вы написали книгу о scala, у вас есть блог или что-то в этом роде?
Томер Бен Дэвид

1
@coolbreeze Может быть, я не ясно выразился. Я имел в виду, что yieldпредложение is customer.value, тип которого Int, поэтому все for comprehensionоценивается как a List[Int].
pagoda_5b

7

Я не scala mega mind, так что не стесняйтесь поправлять меня, но вот как я объясняю flatMap/map/for-comprehensionсагу себе!

Чтобы понять for comprehensionи это перевести, scala's map / flatMapмы должны делать небольшие шаги и понимать составные части - mapи flatMap. Но не scala's flatMapтолько mapсflatten вами спросите себя! если да, то почему так многим разработчикам так трудно понять это или понять for-comprehension / flatMap / map. Что ж, если вы просто посмотрите на scala mapи flatMapподпись, вы увидите, что они возвращают один и тот же тип возврата M[B]и работают с одним и тем же входным аргументом A(по крайней мере, первая часть функции, которую они принимают), если это так, в чем разница?

Наш план

  1. Понять scala's map.
  2. Понять scala's flatMap.
  3. Понять scala 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]вы не можете контролировать. Подчеркнем еще раз:

  1. mapвыбирает для нас выходной контейнер, и это будет тот же контейнер, что и источник, над которым мы работаем, поэтому для M[A]контейнера мы получаем тот же Mконтейнер только дляB M[B] и ничего больше!
  2. 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 и flatMapflatMap, которые мы предоставляем с функцией, которая не только преобразует, 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)

Что у нас здесь:

  1. mkMatcherвозвращает containerконтейнер, содержащий функцию:String => Boolean
  2. Правила таковы, если у нас есть несколько, в которые <-они переводятся, за flatMapисключением последнего.
  3. Поскольку f <- mkMatcher(pat)сначала sequence(подумайте assembly line) все, что мы хотим от этого, - это взять fи передать его следующему рабочему на сборочной линии, мы даем следующему рабочему на нашей сборочной линии (следующая функция) возможность определить, что будет упаковка обратно нашего товара, поэтому последняя функцияmap .
  4. Последний g <- mkMatcher(pat2)будет использовать mapего, потому что он последний на конвейере! так что он может просто выполнить последнюю операцию, с map( g =>которой да! вытаскивает gи использует то, fчто уже было вытащено из контейнераflatMap поэтому мы получаем сначала:

    mkMatcher (pat) flatMap (f // вытащить функцию f дать элемент следующему работнику сборочной линии (вы видите, что у него есть доступ f, и не упаковывать его обратно, я имею в виду, пусть карта определяет упаковку, пусть следующий рабочий сборочной линии определяет container. mkMatcher (pat2) map (g => f (s) ...)) // поскольку это последняя функция в сборочной линии, мы собираемся использовать карту и вытащить g из контейнера и обратно в упаковку , его mapи эта упаковка будет дросселировать полностью и быть нашей упаковкой или нашим контейнером, да!


4

Обоснование состоит в том, чтобы объединить монадические операции в цепочку, что дает в качестве преимущества надлежащую «быструю обработку ошибок».

На самом деле это довольно просто. mkMatcherМетод возвращает Option(который является Монада). Результатом mkMatcherмонадической операции является либо a, Noneлибо aSome(x) .

Применение функции mapили flatMapк a Noneвсегда возвращает 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блока?
Malte Schwerhoff

1
flatMapожидает, что вы передадите функцию, возвращающую результат, «завернутый» / поднятый в монаду, в то время как сам mapсделает упаковку / подъем. Во время цепочки вызовов операций for comprehensionвам необходимо, flatmapчтобы функции, переданные в качестве параметра, могли возвращаться None(вы не можете поднять значение до None). Предполагается, что последний вызов операции, тот, который yieldнаходится в, будет запущен и вернет значение; a, mapчтобы связать эту последнюю операцию, достаточно и позволяет избежать необходимости поднимать результат функции в монаду.
Бруно Гридер

1

Это можно перевести как:

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


0

Во-первых, 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создается новая функция, которая принимает String sи проверяет, sсоответствует ли шаблон.

(s: String) => p.matcher(s).matches

Обратите внимание, что pпеременная привязана к скомпилированному шаблону. Теперь ясно, как создается функция с подписью с String => Booleanпомощью mkMatcher.

Далее давайте проверим bothMatchфункцию, в основе которой лежит mkMatcher. Чтобы показать, как bothMathchработает, сначала посмотрим на эту часть:

mkMatcher(pat2) map (g => f(s) && g(s))

Поскольку у нас есть функция с сигнатурой String => Booleanfrom mkMatcher, которая gв этом контексте g(s)эквивалентна Pattern.compile(pat2).macher(s).matches, которая возвращает, если String s соответствует шаблону pat2. Так как насчет f(s)того же g(s), с той лишь разницей, что при первом вызове mkMatcheruse flatMapвместо « mapПочему»? Поскольку mkMatcher(pat2) map (g => ....)возвращается Option[Boolean], вы получите вложенный результат, Option[Option[Boolean]]если используете mapдля обоих вызовов, это не то, что вы хотите.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.