Плутається з розумінням для перетворення flatMap / Map


87

Я справді не розумію 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.

Відповіді:


199

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)

map and 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-only з одним <-перетворюється у 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залишається a 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 має бути a List[Int]


1
Слова "те саме, що" належать до мета-мови і їх слід перемістити з кодового блоку.
день

3
Кожен початківець FP повинен прочитати це. Як цього можна досягти?
mert inan

1
@melston Давайте наведемо приклад з Lists. Якщо ви mapдвічі A => List[B]виконуєте функцію (яка є однією з найважливіших монадичних операцій) над деяким значенням, ви отримуєте List [List [B]] (ми приймаємо як належне, що типи збігаються). Внутрішній цикл для розуміння складає ці функції з відповідною flatMapоперацією, "сплющуючи" форму Списку [Список [B]] у простий Список [B] ... Сподіваюся, це зрозуміло
pagoda_5b

1
це була чиста дивовижність читання вашої відповіді. Я би хотів, щоб ви написали книгу про Скалу, у вас є щоденник чи щось інше?
Томер Бен Девід,

1
@coolbreeze Може бути, я не чітко висловив це. Я мав на увазі, що це yieldречення customer.value, чий тип Int, тому ціле for comprehensionоцінюється як a List[Int].
pagoda_5b

7

Я не скала-мега розум, тому сміливо виправляйте мене, але так я пояснюю собі 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. Зрозумійте масштаби map.
  2. Зрозумійте масштаби flatMap.
  3. Розуміти в Scala 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контейнер лише для B M[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]

Ви бачите велику різницю від карти до 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, і не пакуйте його назад, я маю на увазі, нехай карта визначає упаковку, хай наступний працівник конвеєра визначає контейнер. mkMatcher (pat2) map (g => f (s) ...)) // оскільки це остання функція у складальній лінії, яку ми збираємось використовувати map і витягнути g з контейнера та назад до упаковки , його mapта ця упаковка буде забиватися до упору і буде нашим пакунком чи контейнером, так!


4

Обґрунтуванням є створення ланцюжків монадичних операцій, що забезпечує як перевагу належну обробку помилок "швидкого збою".

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

Застосування функції mapабо flatMapдо функції a Noneзавжди повертає a None- функцію, передану як параметр mapі flatMapне обчислювану.

Отже, у вашому прикладі, якщо mkMatcher(pat)повертає значення None, застосована до нього flatMap повертає a None(друга монадична операція mkMatcher(pat2)не буде виконана), а final mapповертає a 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до ланцюга, що остання операція є достатньою, і уникає необхідності піднімати результат функції в монаду.
Bruno Grieder

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, будується нова функція, яка приймає рядок 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обидва виклики, це не те, що ви хочете.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.