Lúng túng với việc chuyển đổi bản đồ / bản đồ để hiểu sang bản đồ phẳng


87

Tôi thực sự có vẻ không hiểu về Map và FlatMap. Điều tôi không hiểu là làm thế nào để hiểu là một chuỗi các lệnh gọi lồng nhau đến bản đồ và bản đồ phẳng. Ví dụ sau đây là từ Lập trình chức năng trong Scala

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
            f <- mkMatcher(pat)
            g <- mkMatcher(pat2)
 } yield f(s) && g(s)

Dịch sang

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = 
         mkMatcher(pat) flatMap (f => 
         mkMatcher(pat2) map (g => f(s) && g(s)))

Phương thức mkMatcher được định nghĩa như sau:

  def mkMatcher(pat:String):Option[String => Boolean] = 
             pattern(pat) map (p => (s:String) => p.matcher(s).matches)

Và phương pháp mẫu như sau:

import java.util.regex._

def pattern(s:String):Option[Pattern] = 
  try {
        Some(Pattern.compile(s))
   }catch{
       case e: PatternSyntaxException => None
   }

Sẽ thật tuyệt nếu ai đó có thể làm sáng tỏ cơ sở lý luận đằng sau việc sử dụng map và flatMap tại đây.

Câu trả lời:


200

TL; DR đi thẳng đến ví dụ cuối cùng

Tôi sẽ thử và tóm tắt lại.

Định nghĩa

Phần forhiểu là một phím tắt cú pháp để kết hợp flatMapmaptheo cách dễ đọc và lý luận.

Hãy đơn giản hóa mọi thứ một chút và giả sử rằng mọi classthứ cung cấp cả hai phương thức nói trên đều có thể được gọi là a monadvà chúng ta sẽ sử dụng ký hiệu M[A]để có nghĩa là a monadvới một kiểu bên trong A.

Ví dụ

Một số monads thường thấy bao gồm:

  • List[String] Ở đâu
    • M[X] = List[X]
    • A = String
  • Option[Int] Ở đâu
    • M[X] = Option[X]
    • A = Int
  • Future[String => Boolean] Ở đâu
    • M[X] = Future[X]
    • A = (String => Boolean)

bản đồ và bản đồ phẳng

Được xác định trong một đơn nguyên chung 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]

ví dụ

  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)

để diễn đạt

  1. Mỗi dòng trong biểu thức sử dụng <-ký hiệu được dịch thành một flatMaplệnh gọi, ngoại trừ dòng cuối cùng được dịch thành maplệnh gọi kết thúc , trong đó "biểu tượng liên kết" ở phía bên trái được truyền làm tham số cho hàm đối số (cái gì trước đây chúng tôi đã gọi 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. Biểu thức for chỉ có một <-được chuyển đổi thành maplệnh gọi với biểu thức được truyền dưới dạng đối số:

    // 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
    

Bây giờ đến điểm

Như bạn có thể thấy, phép maptoán bảo toàn "hình dạng" của bản gốc monad, vì vậy điều tương tự cũng xảy ra đối với yieldbiểu thức: a Listvẫn là a Listvới nội dung được biến đổi bởi phép toán trong yield.

Mặt khác, mỗi đường ràng buộc trong forchỉ là một thành phần của liên tiếp monads, phải được "làm phẳng" để duy trì một "hình dạng bên ngoài" duy nhất.

Giả sử trong một khoảnh khắc rằng mỗi ràng buộc bên trong được dịch thành một maplệnh gọi, nhưng bên phải có cùng A => M[B]chức năng, bạn sẽ kết thúc bằng một M[M[B]]cho mỗi dòng trong phần hiểu.
Mục đích của toàn bộ forcú pháp là để dễ dàng "san bằng" việc nối các phép toán đơn nguyên liên tiếp (tức là các phép toán "nâng" một giá trị trong "hình dạng đơn nguyên" A => M[B]:), với việc bổ sung một mapphép toán cuối cùng có thể thực hiện một chuyển đổi kết thúc.

Tôi hy vọng điều này giải thích logic đằng sau sự lựa chọn dịch, được áp dụng một cách máy móc, đó là: n flatMapcác lệnh gọi lồng nhau được kết luận bởi một maplệnh gọi duy nhất .

Một ví dụ minh họa có sẵn
Có nghĩa là để chỉ ra tính biểu cảm của forcú pháp

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 (_ + _)
}

Bạn có thể đoán loại valuesList?

Như đã nói, hình dạng của dấu monadđược duy trì thông qua việc hiểu, vì vậy chúng ta bắt đầu bằng chữ Listin company.branches, và phải kết thúc bằng chữ a List.
Thay vào đó, loại bên trong sẽ thay đổi và được xác định bởi yieldbiểu thức: làcustomer.value: Int

valueList nên là một List[Int]


1
Các từ "giống như" thuộc về siêu ngôn ngữ và nên được chuyển ra khỏi khối mã.
ngày

3
Mọi người mới bắt đầu FP nên đọc điều này. Làm thế nào điều này có thể đạt được?
mert inan

1
@melston Hãy làm một ví dụ với Lists. Nếu bạn sử dụng maphai lần một hàm A => List[B](là một trong những thao tác đơn nguyên cần thiết) trên một giá trị nào đó, bạn sẽ nhận được một Danh sách [Danh sách [B]] (chúng tôi cho rằng các kiểu khớp nhau). Cho sự hiểu biết vòng lặp bên trong soạn những chức năng với tương ứng flatMaphoạt động, "làm phẳng" hình dạng Danh sách [] Danh sách [B] vào một danh sách đơn giản [B] ... Tôi hy vọng điều này là rõ ràng
pagoda_5b

1
thật tuyệt vời khi đọc câu trả lời của bạn. Tôi ước bạn sẽ viết một cuốn sách về scala, bạn có blog hay gì đó không?
Tomer Ben David

1
@coolbreeze Có thể là tôi đã không diễn đạt rõ ràng. Ý tôi muốn nói là yieldmệnh đề customer.value, có kiểu là gì Int, do đó toàn bộ for comprehensionđánh giá là a List[Int].
Pagoda_5b

7

Tôi không phải là người có đầu óc siêu phàm nên cứ sửa cho tôi, nhưng đây là cách tôi giải thích flatMap/map/for-comprehensioncâu chuyện cho chính mình!

Để hiểu for comprehensionvà dịch nó sang, scala's map / flatMapchúng ta phải thực hiện các bước nhỏ và hiểu các phần soạn thảo - mapflatMap. Nhưng không scala's flatMapchỉ mapvới flattenbạn hỏi bản thân mình! nếu vậy tại sao nhiều nhà phát triển lại cảm thấy rất khó để nắm bắt được nó hoặc của for-comprehension / flatMap / map. Chà, nếu bạn chỉ nhìn vào scala mapflatMapchữ ký, bạn sẽ thấy chúng trả về cùng một kiểu trả về M[B]và chúng hoạt động trên cùng một đối số đầu vào A(ít nhất là phần đầu tiên của hàm mà chúng đảm nhận), vậy điều gì tạo nên sự khác biệt?

Kế hoạch của chúng tôi

  1. Hiểu rõ về scala map.
  2. Hiểu rõ về scala flatMap.
  3. Hiểu scala của for comprehension.`

Bản đồ của Scala

chữ ký bản đồ scala:

map[B](f: (A) => B): M[B]

Nhưng có một phần lớn bị thiếu khi chúng ta nhìn vào chữ ký này, và đó là - điều này Ađến từ đâu? vùng chứa của chúng ta thuộc loại Avì vậy điều quan trọng là phải xem chức năng này trong ngữ cảnh của vùng chứa - M[A]. Vùng chứa của chúng ta có thể là một Listloại mục Amaphàm của chúng ta có một hàm chuyển đổi từng loại mục Athành loại B, sau đó nó trả về một vùng chứa loại B(hoặc M[B])

Hãy viết chữ ký của bản đồ có tính đến vùng chứa:

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]

Lưu ý một thực tế cực kỳ quan trọng về bản đồ - nó tự động đóng gói trong vùng chứa đầu ra M[B]mà bạn không có quyền kiểm soát nó. Chúng ta hãy nhấn mạnh nó một lần nữa:

  1. mapchọn vùng chứa đầu ra cho chúng ta và nó sẽ là vùng chứa giống như nguồn mà chúng ta đang làm việc, vì vậy đối với vùng M[A]chứa, chúng ta chỉ nhận được cùng một vùng Mchứa B M[B]và không có gì khác!
  2. mapsự chứa đựng này đối với chúng tôi, chúng tôi chỉ đưa ra một ánh xạ từ Ađến Bvà nó sẽ đặt nó vào hộp của M[B]sẽ đưa nó vào hộp cho chúng tôi!

Bạn thấy bạn đã không chỉ định làm thế nào để containerizemục bạn chỉ định làm thế nào để chuyển đổi các mục bên trong. Và vì chúng ta có cùng một vùng chứa Mcho cả hai M[A]M[B]phương tiện M[B]này là cùng một vùng chứa, nghĩa là nếu bạn có List[A]thì bạn sẽ có một List[B]và quan trọng hơn maplà làm điều đó cho bạn!

Bây giờ chúng ta đã xử lý xong, maphãy chuyển sang flatMap.

Scala's flatMap

Hãy xem chữ ký của nó:

flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]

Bạn thấy sự khác biệt lớn từ bản đồ đến flatMaptrong flatMap mà chúng tôi đang cung cấp cho nó với chức năng không chỉ chuyển đổi từ A to Bmà còn chứa nó thành M[B].

tại sao chúng ta lại quan tâm ai là người thực hiện công việc chứa đựng?

Vậy tại sao chúng ta lại quan tâm nhiều đến chức năng đầu vào để map / flatMap thực hiện việc chứa trong đó M[B]hoặc bản thân bản đồ thực hiện việc chứa cho chúng ta?

Bạn thấy trong bối cảnh của for comprehensionnhững gì đang xảy ra là nhiều biến đổi trên mặt hàng được cung cấp trong forđó, vì vậy chúng tôi sẽ cho công nhân tiếp theo trong dây chuyền lắp ráp của chúng tôi khả năng xác định bao bì. Hãy tưởng tượng chúng ta có một dây chuyền lắp ráp, mỗi công nhân làm một việc gì đó với sản phẩm và chỉ công nhân cuối cùng đóng gói nó trong một thùng chứa! Chào mừng bạn đến với flatMapđây là mục đích của nó, trong mapmỗi công nhân khi làm việc xong vật phẩm cũng đóng gói nó để bạn lấy các thùng chứa trên các thùng chứa.

Sức mạnh để hiểu

Bây giờ chúng ta hãy xem xét sự hiểu biết của bạn có tính đến những gì chúng tôi đã nói ở trên:

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)   
    g <- mkMatcher(pat2)
} yield f(s) && g(s)

Chúng ta có gì ở đây nào:

  1. mkMatchertrả về một containervùng chứa chứa một hàm:String => Boolean
  2. Các quy tắc là nếu chúng tôi có nhiều <-họ dịch sang flatMapngoại trừ cái cuối cùng.
  3. Như phần f <- mkMatcher(pat)đầu tiên trong sequence(nghĩ assembly line) tất cả những gì chúng tôi muốn là lấy fvà chuyển nó cho công nhân tiếp theo trong dây chuyền lắp ráp, chúng tôi cho phép công nhân tiếp theo trong dây chuyền lắp ráp của chúng tôi (chức năng tiếp theo) có khả năng xác định đâu sẽ là đóng gói mặt hàng của chúng tôi đây là lý do tại sao chức năng cuối cùng là map.
  4. Cuối cùng g <- mkMatcher(pat2)sẽ sử dụng mapđiều này là vì cuối cùng của nó trong dây chuyền lắp ráp! vì vậy nó chỉ có thể thực hiện thao tác cuối cùng map( g =>mà có! kéo ra gvà sử dụng fcái đã được kéo ra khỏi thùng chứa do flatMapđó chúng ta kết thúc với đầu tiên:

    mkMatcher (pat) flatMap (f // pull out f function đưa item cho công nhân dây chuyền lắp ráp tiếp theo (bạn thấy nó có quyền truy cập fvà không đóng gói nó lại, ý tôi là hãy để bản đồ xác định cách đóng gói để công nhân dây chuyền lắp ráp tiếp theo xác định container. mkMatcher (pat2) map (g => f (s) ...)) // vì đây là hàm cuối cùng trong dây chuyền lắp ráp, chúng ta sẽ sử dụng map và kéo g ra khỏi container và quay trở lại bao bì , mapbao bì của nó và bao bì này sẽ tăng tốc hết cỡ và là gói hàng hoặc thùng chứa của chúng ta, yah!


4

Cơ sở lý luận là chuỗi hoạt động đơn nguyên mang lại lợi ích, xử lý lỗi "fail fast" thích hợp.

Nó thực sự khá đơn giản. mkMatcherPhương thức trả về một Option(là Đơn nguyên). Kết quả của mkMatcher, hoạt động đơn nguyên, là a Nonehoặc a Some(x).

Việc áp dụng hàm maphoặc flatMapcho a Noneluôn trả về a None- hàm được truyền dưới dạng tham số mapflatMapkhông được đánh giá.

Do đó, trong ví dụ của bạn, nếu mkMatcher(pat)trả về Không có, Bản đồ phẳng được áp dụng cho nó sẽ trả về a None(hoạt động đơn nguyên thứ hai mkMatcher(pat2)sẽ không được thực hiện) và thao tác cuối cùng mapsẽ lại trả về a None. Nói cách khác, nếu bất kỳ hoạt động nào trong hàm cho hiểu, trả về Không có, bạn có một hành vi nhanh không thành công và phần còn lại của các hoạt động không được thực thi.

Đây là phong cách xử lý lỗi đơn nguyên. Kiểu mệnh lệnh sử dụng các ngoại lệ, về cơ bản là các bước nhảy (đến mệnh đề bắt)

Lưu ý cuối cùng: patternshàm là một cách điển hình để "dịch" một cách xử lý lỗi kiểu mệnh lệnh ( try... catch) thành một cách xử lý lỗi kiểu đơn nguyên bằng cách sử dụngOption


Bạn có biết tại sao flatMap(và không map) được sử dụng để "nối" lệnh gọi thứ nhất và thứ hai mkMatcher, nhưng tại sao map(và không flatMap) được sử dụng "nối" lệnh thứ hai mkMatcheryieldskhối?
Malte Schwerhoff

1
flatMapmong đợi bạn chuyển một hàm trả về kết quả "wrap" / lift trong Monad, trong khi mapsẽ tự thực hiện việc gói / nâng. Trong quá trình gọi chuỗi các hoạt động trong for comprehensionbạn cần để flatmapcác hàm được truyền dưới dạng tham số có thể trả về None(bạn không thể nâng giá trị thành Không có). Lệnh gọi hoạt động cuối cùng, lệnh gọi trong lệnh yieldsẽ chạy trả về một giá trị; a mapđể xâu chuỗi rằng hoạt động cuối cùng là đủ và tránh phải nâng kết quả của chức năng vào đơn nguyên.
Bruno Grieder

1

Điều này có thể được tổng hợp như sau:

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)

Chạy cái này để có cái nhìn rõ hơn về cách nó mở rộng

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)

kết quả là:

1 -> a
1 -> b
1 -> c
...
2 -> a
2 -> b
...

Điều này tương tự như flatMap- lặp qua từng phần tử trong patvà chuyển tiếp phần tử mapđó đến từng phần tử trongpat2


0

Đầu tiên, mkMatchertrả về một hàm có chữ ký String => Boolean, đó là một thủ tục java thông thường vừa chạy Pattern.compile(string), như được hiển thị trong patternhàm. Sau đó, nhìn vào dòng này

pattern(pat) map (p => (s:String) => p.matcher(s).matches)

Các mapchức năng được áp dụng cho các kết quả của pattern, đó là Option[Pattern], vì vậy ptrong p => xxxchỉ là mẫu bạn biên soạn. Vì vậy, với một mẫu p, một hàm mới sẽ được xây dựng, lấy một Chuỗi svà kiểm tra xem có skhớp với mẫu không.

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

Lưu ý, pbiến được liên kết với mẫu đã biên dịch. Bây giờ, rõ ràng rằng cách một hàm có chữ ký String => Booleanđược xây dựng bởi mkMatcher.

Tiếp theo, hãy kiểm tra bothMatchchức năng dựa trên mkMatcher. Để hiển thị cách bothMathchhoạt động, trước tiên chúng ta xem xét phần này:

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

Vì chúng ta có một hàm có chữ ký String => Booleantừ mkMatcher, gtrong ngữ cảnh này, g(s)tương đương với Pattern.compile(pat2).macher(s).matches, hàm này sẽ trả về nếu chuỗi s khớp với mẫu pat2. Vì vậy, làm thế nào về f(s), nó giống như g(s), sự khác biệt duy nhất là, lần gọi đầu tiên của các mkMatchersử dụng flatMap, thay vì map, Tại sao? Bởi vì mkMatcher(pat2) map (g => ....)trả về Option[Boolean], bạn sẽ nhận được một kết quả lồng nhau Option[Option[Boolean]]nếu bạn sử dụng mapcho cả hai cuộc gọi, đó không phải là điều bạn muốn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.