Bingung dengan pemahaman-for ke transformasi flatMap / Map

87

Saya benar-benar tidak memahami Map dan FlatMap. Yang gagal saya pahami adalah bagaimana pemahaman-for adalah urutan panggilan bersarang ke map dan flatMap. Contoh berikut adalah dari Pemrograman Fungsional di Scala

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

diterjemahkan menjadi

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

Metode mkMatcher didefinisikan sebagai berikut:

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

Dan metode polanya adalah sebagai berikut:

import java.util.regex._

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

Akan sangat bagus jika seseorang dapat menjelaskan alasan di balik penggunaan map dan flatMap di sini.

sc_ray
sumber

Jawaban:

201

TL; DR langsung ke contoh terakhir

Saya akan mencoba dan merekap.

Definisi

The forpemahaman adalah cara pintas sintaks untuk menggabungkan flatMapdan mapdengan cara yang mudah dibaca dan alasan tentang.

Mari kita sederhanakan sedikit dan asumsikan bahwa setiap classyang menyediakan kedua metode yang disebutkan di atas dapat disebut a monaddan kita akan menggunakan simbol yang M[A]berarti a monaddengan tipe bagian dalam A.

Contoh

Beberapa monad yang biasa terlihat meliputi:

  • List[String] dimana
    • M[X] = List[X]
    • A = String
  • Option[Int] dimana
    • M[X] = Option[X]
    • A = Int
  • Future[String => Boolean] dimana
    • M[X] = Future[X]
    • A = (String => Boolean)

map dan flatMap

Didefinisikan dalam monad generik 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]

misalnya

  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)

untuk ekspresi

  1. Setiap baris dalam ekspresi yang menggunakan <-simbol diterjemahkan menjadi flatMappanggilan, kecuali baris terakhir yang diterjemahkan menjadi mappanggilan penutup , di mana "simbol terikat" di sisi kiri diteruskan sebagai parameter ke fungsi argumen (apa kami sebelumnya menelepon 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. Sebuah ekspresi-untuk dengan hanya satu <-akan diubah menjadi mappanggilan dengan ekspresi yang diteruskan sebagai argumen:

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

Sekarang langsung ke intinya

Seperti yang Anda lihat, mapoperasi tersebut mempertahankan "bentuk" aslinya monad, sehingga hal yang sama terjadi untuk yieldekspresi: a Listtetap a Listdengan konten yang diubah oleh operasi di yield.

Di sisi lain, setiap garis pengikat di dalam forhanyalah komposisi yang berurutanmonads , yang harus "diratakan" untuk mempertahankan satu "bentuk luar".

Misalkan sejenak bahwa setiap pengikatan internal diterjemahkan menjadi mappanggilan, tetapi tangan kanan memiliki A => M[B]fungsi yang sama , Anda akan berakhir dengan a M[M[B]]untuk setiap baris dalam pemahaman.
Maksud dari keseluruhan forsintaks adalah untuk dengan mudah "meratakan" rangkaian operasi monadik yang berurutan (yaitu, operasi yang "mengangkat" nilai dalam "bentuk monadik":) A => M[B], dengan penambahan mapoperasi akhir yang mungkin melakukan transformasi penutup.

Saya harap ini menjelaskan logika di balik pilihan terjemahan, yang diterapkan secara mekanis, yaitu: n flatMappanggilan bersarang yang diakhiri dengan satu mappanggilan.

Contoh ilustratif yang dibuat-buat.
Dimaksudkan untuk menunjukkan ekspresi forsintaksis

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

Bisakah Anda menebak jenisnya valuesList ?

Seperti yang sudah dikatakan, bentuk dari yang monaddipertahankan melalui pemahaman, jadi kita mulai dengan Listdalam company.branches, dan harus diakhiri dengan a List.
Tipe bagian dalam berubah dan ditentukan oleh yieldekspresi: yaitucustomer.value: Int

valueList harus menjadi List[Int]

pagoda_5b
sumber
1
Kata-kata "sama dengan" termasuk dalam bahasa meta dan harus dipindahkan dari blok kode.
hari
3
Setiap pemula FP harus membaca ini. Bagaimana ini bisa dicapai?
mert inan
1
@melston Mari kita buat contoh dengan Lists. Jika Anda mapmenggandakan fungsi A => List[B](yang merupakan salah satu operasi monadik esensial) pada beberapa nilai, Anda akan mendapatkan List [List [B]] (kami menerima begitu saja bahwa jenisnya cocok). Loop dalam untuk pemahaman menyusun fungsi-fungsi tersebut dengan flatMapoperasi yang sesuai , "meratakan" bentuk Daftar [Daftar [B]] menjadi Daftar [B] sederhana ... Saya harap ini jelas
pagoda_5b
1
sungguh luar biasa membaca jawaban Anda. Saya harap Anda mau menulis buku tentang scala, apakah Anda punya blog atau semacamnya?
Tomer Ben David
1
@coolbreeze Bisa jadi saya tidak mengungkapkannya dengan jelas. Yang saya maksudkan adalah yieldklausa tersebut customer.value, yang tipenya Int, oleh karena itu keseluruhannya for comprehensionbernilai a List[Int].
pagoda_5b
7

Saya bukan scala mega mind jadi jangan ragu untuk mengoreksi saya, tapi beginilah cara saya menjelaskan flatMap/map/for-comprehensionsaga ini kepada diri saya sendiri!

Untuk memahami for comprehensiondan menerjemahkannya, scala's map / flatMapkita harus mengambil langkah kecil dan memahami bagian penyusunnya - mapdan flatMap. Tapi tidakscala's flatMap hanya mapdengan flattenAnda bertanya pada diri sendiri! Jika demikian, mengapa begitu banyak pengembang merasa begitu sulit untuk memahaminya atau tentangnya for-comprehension / flatMap / map. Nah, jika Anda hanya melihat pada scala mapdan flatMaptanda tangan Anda melihat mereka mengembalikan tipe pengembalian yang sama M[B]dan mereka bekerja pada argumen input yang sama A(setidaknya bagian pertama dari fungsi yang mereka ambil) jika demikian, apa yang membuat perbedaan?

Rencana kita

  1. Pahami scala map.
  2. Pahami scala flatMap.
  3. Pahami scala's for comprehension.`

Peta Scala

tanda peta skala:

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

Tetapi ada bagian besar yang hilang ketika kita melihat tanda tangan ini, dan ini - dari mana Aasalnya? penampung kita adalah tipe Ajadi penting untuk melihat fungsi ini dalam konteks penampung - M[A]. Wadah kita bisa berupa Listitem bertipe Adan mapfungsi kita mengambil fungsi yang mengubah setiap item dari tipe Amenjadi tipe B, lalu mengembalikan kontainer tipe B(atau M[B])

Mari kita tulis tanda tangan peta dengan mempertimbangkan wadah:

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]

Catat fakta yang sangat sangat sangat penting tentang map - peta itu otomatis membundel dalam wadah keluaran yang M[B]tidak dapat Anda kendalikan. Mari kita tekankan lagi:

  1. map memilih wadah keluaran untuk kita dan itu akan menjadi wadah yang sama dengan sumber yang kita kerjakan untuk itu M[A] wadah kita mendapatkan Mwadah yang sama hanya untukB M[B] dan tidak ada yang lain!
  2. mapapakah containerization ini untuk kami, kami hanya memberikan pemetaan dari AmenjadiB dan itu akan memasukkannya ke dalam kotakM[B] akan meletakkannya di dalam kotak untuk kita!

Anda lihat Anda tidak menentukan bagaimana containerizeitem yang baru saja Anda tentukan bagaimana mengubah item internal. Dan karena kami memiliki wadah yang sama Muntuk keduanya M[A]dan M[B]ini berarti M[B]wadah yang sama, artinya jika Anda memilikinya List[A]maka Anda akan memilikiList[B] dan yang lebih penting mapmelakukannya untuk Anda!

Sekarang kita sudah berurusan dengan map mari kita lanjutkan ke flatMap.

FlatMap Scala

Mari kita lihat tanda tangannya:

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

Anda melihat perbedaan besar dari map ke flatMapdi flatMap kami menyediakannya dengan fungsi yang tidak hanya mengonversi A to Btetapi juga memasukkannya ke dalam container M[B].

mengapa kita peduli siapa yang melakukan containerization?

Jadi mengapa kita sangat memperhatikan fungsi input untuk map / flatMap apakah containerization menjadi M[B]atau map itu sendiri melakukan containerization untuk kita?

Anda lihat dalam konteks for comprehensionapa yang terjadi adalah beberapa transformasi pada item yang disediakan di forjadi kami memberikan pekerja berikutnya di jalur perakitan kami kemampuan untuk menentukan kemasan. bayangkan kita memiliki jalur perakitan, setiap pekerja melakukan sesuatu pada produk dan hanya pekerja terakhir yang mengemasnya dalam sebuah wadah! selamat datang flatMapini adalah tujuannya, dimap setiap pekerja ketika selesai mengerjakan item juga mengemasnya sehingga Anda mendapatkan kontainer di atas kontainer.

Yang perkasa untuk pemahaman

Sekarang mari kita lihat pemahaman Anda dengan mempertimbangkan apa yang kami katakan di atas:

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

Apa yang kita dapatkan disini:

  1. mkMatchermengembalikan containerwadah berisi fungsi:String => Boolean
  2. Aturannya adalah jika kita memiliki beberapa yang <-mereka terjemahkan flatMapkecuali yang terakhir.
  3. Seperti f <- mkMatcher(pat)yang pertama dalam sequence(pikirkan assembly line) semua yang kami inginkan adalah mengambil fdan meneruskannya ke pekerja berikutnya di jalur perakitan, kami membiarkan pekerja berikutnya di jalur perakitan kami (fungsi selanjutnya) kemampuan untuk menentukan apa yang akan menjadi mengemas kembali barang kami inilah mengapa fungsi terakhir adalahmap .
  4. Yang terakhir g <- mkMatcher(pat2)akan menggunakan mapini karena yang terakhir di jalur perakitan! sehingga dapat melakukan operasi terakhir dengan map( g =>ya! menarik keluar gdan menggunakan fyang telah ditarik keluar dari wadah oleh flatMapkarena itu kita berakhir dengan yang pertama:

    mkMatcher (pat) flatMap (f // pull out fungsi f memberikan item kepada pekerja lini perakitan berikutnya (Anda melihatnya memiliki akses ke f, dan jangan memaketkannya kembali maksud saya biarkan peta menentukan kemasannya biarkan pekerja jalur perakitan berikutnya menentukan container. mkMatcher (pat2) map (g => f (s) ...)) // karena ini adalah fungsi terakhir di jalur perakitan, kita akan menggunakan map dan menarik g keluar dari container dan ke pengemasan kembali , its mapdan packaging ini akan mempercepat semua dan menjadi paket atau wadah kami, yah!

Tomer Ben David
sumber
4

Alasannya adalah untuk merantai operasi monad yang memberikan manfaat, penanganan error "gagal cepat" yang tepat.

Ini sebenarnya sangat sederhana. The mkMatcherMetode mengembalikan sebuah Option(yang merupakan Monad). Hasil dari mkMatcher, operasi monadik, adalah a Noneatau a Some(x).

Menerapkan mapatau flatMapfungsi ke Noneselalu mengembalikan a None- fungsi yang diteruskan sebagai parameter ke mapdan flatMaptidak dievaluasi.

Karenanya dalam contoh Anda, jika mkMatcher(pat)mengembalikan Tidak Ada, flatMap yang diterapkan padanya akan mengembalikan a None(operasi monadik kedua mkMatcher(pat2)tidak akan dijalankan) dan final mapakan kembali lagi a None. Dengan kata lain, jika salah satu operasi dalam pemahaman for, mengembalikan Tidak ada, Anda memiliki perilaku cepat gagal dan operasi lainnya tidak dijalankan.

Ini adalah gaya penanganan error yang monadik. Gaya imperatif menggunakan pengecualian, yang pada dasarnya adalah lompatan (ke klausa tangkapan)

Catatan terakhir: patternsfungsinya adalah cara khas untuk "menerjemahkan" penanganan kesalahan gaya imperatif ( try... catch) ke penanganan kesalahan gaya monadik menggunakanOption

Bruno Grieder
sumber
Apakah Anda tahu mengapa flatMap(dan tidak map) digunakan untuk "menggabungkan" pemanggilan pertama dan kedua mkMatcher, tetapi mengapa map(dan tidak flatMap) digunakan "menggabungkan" yang kedua mkMatcherdan yieldsblok?
Malte Schwerhoff
1
flatMapmengharapkan Anda untuk melewatkan fungsi yang mengembalikan hasil "dibungkus" / diangkat di Monad, sementara mapakan melakukan pembungkusan / pengangkatan itu sendiri. Selama rangkaian panggilan operasi di for comprehensionAnda perlu flatmapsehingga fungsi yang dilewatkan sebagai parameter dapat kembali None(Anda tidak dapat mengangkat nilai menjadi None). Panggilan operasi terakhir, yang yielddiharapkan untuk menjalankan dan mengembalikan nilai; a mapke rantai bahwa operasi terakhir sudah cukup dan menghindari harus mengangkat hasil fungsi ke monad.
Bruno Grieder
1

Ini dapat diterjemahkan sebagai:

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)

Jalankan ini untuk melihat lebih baik bagaimana ini diperluas

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)

hasilnya adalah:

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

Ini mirip dengan flatMap- loop melalui setiap elemen di patdan mapuntuk setiap elemen ke setiap elemen dipat2

korefn
sumber
0

Pertama, mkMatchermengembalikan fungsi yang tanda tangannya String => Boolean, itu adalah prosedur java biasa yang baru saja dijalankan Pattern.compile(string), seperti yang ditunjukkan di patternfungsi. Lalu, lihat baris ini

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

The mapFungsi diterapkan pada hasil pattern, yang Option[Pattern], sehingga pdalam p => xxxhanya pola Anda dikompilasi. Jadi, dengan adanya pola p, fungsi baru dibangun, yang mengambil String s, dan memeriksa apakah scocok dengan pola tersebut.

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

Catatan, pvariabel dibatasi ke pola yang dikompilasi. Sekarang, jelas bahwa bagaimana sebuah fungsi dengan tanda tangan String => Booleandibangun mkMatcher.

Selanjutnya, mari kita periksa bothMatchfungsinya, yang didasarkan pada mkMatcher. Untuk menunjukkan cara bothMathchkerjanya, pertama-tama kita lihat bagian ini:

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

Karena kita mendapat fungsi dengan tanda tangan String => Booleanfrom mkMatcher, yang gdalam konteks ini, g(s)setara dengan Pattern.compile(pat2).macher(s).matches, yang mengembalikan jika pola String cocok pat2. Jadi bagaimana f(s), sama seperti g(s), satu-satunya perbedaan adalah, panggilan mkMatcherpenggunaan pertama flatMap, bukan map, Mengapa? Karena mkMatcher(pat2) map (g => ....)pengembalian Option[Boolean], Anda akan mendapatkan hasil bersarang Option[Option[Boolean]]jika Anda menggunakan mapuntuk kedua panggilan, bukan itu yang Anda inginkan.

xiaowl
sumber