สับสนกับความเข้าใจในการแปลง flatMap / Map


88

ดูเหมือนฉันจะไม่เข้าใจแผนที่และ FlatMap จริงๆ สิ่งที่ฉันไม่เข้าใจคือความเข้าใจคือลำดับของการเรียกซ้อนไปยังแผนที่และ flatMap อย่างไร ตัวอย่างต่อไปนี้มาจากFunctional Programming ใน 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

คำตอบ:


202

TL; DR ไปที่ตัวอย่างสุดท้ายโดยตรง

ฉันจะพยายามสรุป

คำจำกัดความ

ความforเข้าใจเป็นทางลัดของไวยากรณ์ที่จะรวมเข้าด้วยกันflatMapและmapเป็นวิธีที่ง่ายต่อการอ่านและให้เหตุผล

Let 's สิ่งที่ง่ายบิตและคิดว่าทุกคนclassที่ให้ทั้งสองวิธีดังกล่าวสามารถเรียกmonadและเราจะใช้สัญลักษณ์M[A]หมายถึงกับชนิดภายในmonadA

ตัวอย่าง

พระที่พบเห็นได้ทั่วไป ได้แก่ :

  • 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

กำหนดไว้ใน monad ทั่วไป 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เรียกสรุปโดยที่ "สัญลักษณ์ที่ถูกผูกไว้" ทางด้านซ้ายมือจะถูกส่งไปเป็นพารามิเตอร์ไปยังฟังก์ชันอาร์กิวเมนต์ (what เราโทรไปก่อนหน้านี้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-expression ที่มีเพียงอันเดียว<-จะถูกแปลงเป็นการ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ไวยากรณ์ทั้งหมดคือการ "แบน" การต่อเนื่องกันของการดำเนินการ monadic ที่ต่อเนื่องกันอย่างง่ายดาย (เช่นการดำเนินการที่ "ยก" ค่าในรูปแบบ "monadic shape":) 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
yieldcustomer.value: Int

valueList ควรเป็น List[Int]


1
คำว่า "เหมือนกับ" เป็นของภาษาเมตาและควรย้ายออกจากบล็อกโค้ด
วันที่

3
ผู้เริ่มต้น FP ทุกคนควรอ่านสิ่งนี้ จะทำได้อย่างไร?
mert inan

1
@melston ขอยกตัวอย่างด้วยLists. หากคุณmapใช้ฟังก์ชันสองครั้งA => List[B](ซึ่งเป็นหนึ่งในการดำเนินการเชิงเดี่ยวที่จำเป็น) มากกว่าค่าบางค่าคุณจะได้รับ List [List [B]] (เรายอมรับว่าประเภทตรงกัน) วงในเพื่อความเข้าใจประกอบด้วยฟังก์ชันเหล่านั้นด้วยการflatMapดำเนินการที่สอดคล้องกัน"การทำให้แบน" รายการ [รายการ [B]] เป็นรูปแบบรายการธรรมดา [B] ... ฉันหวังว่านี่จะชัดเจน
pagoda_5b

1
มันเป็นความสุดยอดจริงๆที่อ่านคำตอบของคุณ ฉันหวังว่าคุณจะเขียนหนังสือเกี่ยวกับสกาล่าคุณมีบล็อกหรืออะไร?
Tomer Ben David

1
@coolbreeze อาจเป็นได้ว่าฉันไม่ได้แสดงออกอย่างชัดเจน สิ่งที่ฉันหมายถึงคือyieldอนุประโยคคือcustomer.valueซึ่งเป็นประเภทIntดังนั้นทั้งหมดจึงfor comprehensionประเมินเป็นList[Int].
เจดีย์ _5b

7

ฉันไม่ใช่คนที่มีจิตใจที่น่ากลัวดังนั้นอย่าลังเลที่จะแก้ไขฉัน แต่นี่คือวิธีที่ฉันอธิบาย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คอนเทนเนอร์เดียวกันเท่านั้นB M[B]และไม่มีอะไรอื่น!
  2. mapcontainerization นี้สำหรับเราเราแค่ให้การทำแผนที่จากAถึงBและมันจะใส่ลงในกล่องM[B]จะใส่ลงในกล่องให้เรา!

คุณเห็นว่าคุณไม่ได้ระบุวิธีการcontainerizeที่คุณเพิ่งระบุวิธีการแปลงไอเท็มภายใน และเนื่องจากเรามีคอนเทนเนอร์เดียวกันMสำหรับทั้งสองอย่างM[A]และM[B]หมายความว่าM[B]เป็นคอนเทนเนอร์เดียวกันหมายความว่าถ้าคุณมีList[A]แล้วคุณจะมีList[B]และที่สำคัญกว่าmapนั้นคือทำเพื่อคุณ!

ตอนนี้เราได้จัดการแล้วmapเรามาดูflatMapกันดีกว่า

FlatMap ของ Scala

มาดูลายเซ็นกัน:

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]ด้วย

ทำไมเราถึงสนใจว่าใครเป็นผู้จัดทำคอนเทนเนอร์?

เหตุใดเราจึงใส่ใจฟังก์ชั่นการป้อนข้อมูลไปยังแผนที่ / 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

เหตุผลคือการเชื่อมโยงการดำเนินการ monadic ซึ่งให้ประโยชน์การจัดการข้อผิดพลาด "ล้มเหลวอย่างรวดเร็ว" ที่เหมาะสม

มันค่อนข้างเรียบง่าย mkMatcherวิธีการส่งกลับOption(ซึ่งเป็น Monad) ผลมาจากmkMatcherการดำเนินการเอกเป็นอย่างใดอย่างหนึ่งหรือNoneSome(x)

การใช้ฟังก์ชันmapor flatMapเพื่อNoneส่งกลับเสมอNone- ฟังก์ชันที่ส่งผ่านเป็นพารามิเตอร์ไปยังmapและflatMapไม่ได้รับการประเมิน

ดังนั้นในตัวอย่างของคุณถ้าmkMatcher(pat)ผลตอบแทนไม่มีที่ flatMap นำไปใช้ก็จะกลับมาเป็นNone(การดำเนินงานเอกที่สองmkMatcher(pat2)จะไม่ได้รับการดำเนินการ) และสุดท้ายอีกครั้งจะกลับมาเป็นmap Noneกล่าวอีกนัยหนึ่งคือถ้าการดำเนินการใด ๆ ในเพื่อความเข้าใจส่งกลับค่าไม่มีแสดงว่าคุณมีพฤติกรรมล้มเหลวอย่างรวดเร็วและการดำเนินการที่เหลือจะไม่ถูกดำเนินการ

นี่คือรูปแบบการจัดการข้อผิดพลาดแบบ monadic รูปแบบที่จำเป็นใช้ข้อยกเว้นซึ่งโดยพื้นฐานแล้วจะเป็นการกระโดด (ไปยังประโยคจับ)

หมายเหตุสุดท้าย: patternsฟังก์ชันนี้เป็นวิธีปกติในการ "แปล" การจัดการข้อผิดพลาดรูปแบบที่จำเป็น ( try... catch) เป็นการจัดการข้อผิดพลาดสไตล์โมโนโดยใช้Option


คุณรู้หรือไม่ว่าเหตุใดจึงใช้flatMap(และไม่map) เพื่อ "เชื่อมต่อ" การเรียกใช้ครั้งแรกและครั้งที่สองmkMatcherแต่ทำไมmap(และไม่flatMap) จึงใช้ "concatenate" ที่สองmkMatcherและyieldsบล็อก
Malte Schwerhoff

1
flatMapคาดว่าคุณจะส่งฟังก์ชันที่ส่งคืนผลลัพธ์ "ห่อ" / ยกใน Monad ในขณะที่mapจะทำการห่อ / ยกเอง ในระหว่างการเชื่อมโยงการโทรของการดำเนินการในสิ่งที่for comprehensionคุณต้องการเพื่อflatmapให้ฟังก์ชันที่ส่งผ่านเป็นพารามิเตอร์สามารถส่งคืนได้None(คุณไม่สามารถยกค่าเป็นไม่มีได้) การเรียกใช้การดำเนินการล่าสุดหนึ่งในyieldคาดว่าจะรันและส่งคืนค่า a mapto chain ว่าการดำเนินการสุดท้ายเพียงพอและหลีกเลี่ยงการยกผลลัพธ์ของฟังก์ชันไปไว้ใน monad
Bruno Grieder

1

สิ่งนี้สามารถ traslated เป็น:

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ขั้นตอนจาวาปกติที่เพิ่งเรียกใช้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ที่ผลตอบแทนถ้า String s pat2รูปแบบการแข่งขัน แล้วf(s)มันก็เหมือนกับg(s)ความแตกต่างเพียงอย่างเดียวคือการเรียกmkMatcherใช้ครั้งแรกflatMapแทนที่จะเป็นmapทำไม? เนื่องจากmkMatcher(pat2) map (g => ....)ผลตอบแทนOption[Boolean]คุณจะได้รับผลลัพธ์ที่ซ้อนกันOption[Option[Boolean]]หากคุณใช้mapสำหรับการโทรทั้งสองไม่ใช่สิ่งที่คุณต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.