Путать с пониманием преобразования 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)

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

  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 выражение: List остается List с содержанием, преобразованным операцией в yield,

С другой стороны, каждая обязательная строка в for это просто композиция из последовательных monadsкоторый должен быть "сплющен", чтобы сохранить единую "внешнюю форму".

Предположим на мгновение, что каждое внутреннее связывание было переведено в map звоните, но правая рука осталась прежней A => M[B] функция, вы бы в конечном итоге M[M[B]] для каждой строки в понимании.
Цель всего for Синтаксис состоит в том, чтобы легко "сгладить" конкатенацию последовательных монадических операций (то есть операций, которые "поднимают" значение в "монадической форме"): A => M[B]), с добавлением финала map операция, которая, возможно, выполняет заключительное преобразование.

Я надеюсь, что это объясняет логику выбора перевода, который применяется механически, то есть: nflatMap вложенные вызовы, заключенные одним 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 (по крайней мере, первая часть функции, которую они принимают), если это так, что имеет значение?

Наш план

  1. Понять скалы map,
  2. Понять скалы flatMap,
  3. Понять скалы 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] Вы не можете это контролировать. Давайте снова подчеркнем это:

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

Что мы получили здесь:

  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и не упаковывать его обратно, я имею в виду, пусть карта определяет упаковку, пусть следующий рабочий сборочной линии определит контейнер. 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 для обоих звонков, это не то, что вы хотите.

Другие вопросы по тегам