Смутно с пониманием преобразования 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] где
    • M[X] = Option[X]
    • A = 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)

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

  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 которая, возможно, выполняет заключительное преобразование.

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