Jika Anda tidak tahu bagaimana mereka berbeda
someMap.map{ case (k, v) => k -> foo(v)}
dan
someMap.mapValues(foo)
kecuali untuk sintaks, atau Anda ragu / tidak tahu apa konsekuensi buruk perbedaan ini dapat menyebabkan, dan di mana identity
, maka artikel ini adalah untuk Anda.
Kalau tidak, ambil bagian dalam survei yang terletak di akhir artikel.
Mari kita mulai dengan yang sederhana
Mari kita coba dengan bodoh untuk mengambil contoh yang terletak sebelum kat dan melihat apa yang terjadi:
val someMap = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): String = value + "_changed" val resultMap = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}")
Kode ini diharapkan untuk dicetak
resultMap: Map(key1 -> value1_changed, key2 -> value2_changed) resultMapValues: Map(key1 -> value1_changed, key2 -> value2_changed) equality: true
Dan pada tingkat ini, pemahaman tentang metode mapValues
pada tahap awal pembelajaran Scala: baik, ya, ada metode seperti itu, akan lebih mudah untuk mengubah nilai-nilai di Map
ketika kunci tidak berubah. Dan sungguh, apa lagi yang ada untuk dipikirkan? Dengan nama metode, semuanya jelas, perilakunya jelas.
Contoh yang lebih rumit
Mari kita sedikit memodifikasi contoh kami (saya akan secara eksplisit menulis jenis sehingga Anda tidak berpikir bahwa ada semacam muggle dengan implisit):
case class ValueHolder(value: String) val someMap: Map[String, String] = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): ValueHolder = ValueHolder(value) val resultMap: Map[String, ValueHolder] = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}")
Dan kode tersebut akan menghasilkan setelah peluncuran
resultMap: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) resultMapValues: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) equality: true
Cukup logis dan jelas. "Sobat, saatnya untuk sampai ke bagian bawah artikel!" - Pembaca akan berkata. Mari kita buat penciptaan kelas kita tergantung pada kondisi eksternal dan menambahkan beberapa pemeriksaan sederhana untuk kebodohan:
case class ValueHolder(value: String, seed: Int) def foo(value: String): ValueHolder = ValueHolder(value, Random.nextInt()) ... println(s"simple assert for resultMap: ${resultMap.head == resultMap.head}") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}")
Pada output yang kita dapatkan:
resultMap: Map(key1 -> ValueHolder(value1,1189482436), key2 -> ValueHolder(value2,-702760039)) resultMapValues: Map(key1 -> ValueHolder(value1,-1354493526), key2 -> ValueHolder(value2,-379389312)) equality: false simple assert for resultMap: true simple assert for resultMapValues: false
Adalah logis bahwa hasilnya sekarang tidak sama, tetapi acak. Tapi tunggu, mengapa pernyataan kedua itu false
? Sungguh nilai dalam resultMapValues
telah berubah, tetapi kami tidak melakukan apa pun dengan mereka? Mari kita periksa apakah semua yang ada di dalamnya sama dengan sebelumnya:
println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues")
Dan pada output kita dapatkan:
resultMapValues: Map(key1 -> ValueHolder(value1,1771067356), key2 -> ValueHolder(value2,2034115276)) resultMapValues: Map(key1 -> ValueHolder(value1,-625731649), key2 -> ValueHolder(value2,-1815306407))

Mengapa ini terjadi?
Mengapa println
mengubah nilai Map
?
Saatnya masuk ke dokumentasi untuk metode mapValues
:
Baris pertama memberi tahu kita apa yang kita pikirkan - ia mengubah Map
, menerapkan fungsi yang diteruskan dalam argumen ke setiap nilai. Tetapi jika Anda membacanya dengan sangat hati-hati dan sampai akhir, ternyata bukan Map
yang dikembalikan, melainkan "tampilan peta" (tampilan). Dan ini bukan tampilan normal ( View
), yang bisa Anda dapatkan menggunakan metode view
dan yang memiliki metode force
untuk perhitungan eksplisit. Kelas khusus (kodenya dari Scala versi 2.12.7, tetapi untuk 2.11 ada hal yang hampir sama):
protected class MappedValues[W](f: V => W) extends AbstractMap[K, W] with DefaultMap[K, W] { override def foreach[U](g: ((K, W)) => U): Unit = for ((k, v) <- self) g((k, f(v))) def iterator = for ((k, v) <- self.iterator) yield (k, f(v)) override def size = self.size override def contains(key: K) = self.contains(key) def get(key: K) = self.get(key).map(f) }
Jika Anda membaca kode ini, Anda akan melihat bahwa tidak ada yang di-cache, dan setiap kali Anda mengakses nilainya, mereka akan dihitung ulang. Apa yang kita amati dalam contoh kita.
Jika Anda bekerja dengan fungsi murni dan semuanya tidak berubah, maka Anda tidak akan melihat perbedaan apa pun. Yah, mungkin kinerja akan menguras. Tetapi, sayangnya, tidak semua yang ada di dunia kita bersih dan sempurna, dan dengan menggunakan metode ini, Anda dapat menginjak rake (yang terjadi di salah satu proyek kami, kalau tidak, artikel ini tidak akan).
Tentu saja, kami bukan yang pertama kali menemukan ini. Sudah pada tahun 2011, bug utama dibuka pada kesempatan ini (dan pada saat penulisan, ini ditandai sebagai terbuka). Itu juga menyebutkan metode filterKeys
, yang memiliki masalah yang persis sama, karena diterapkan pada prinsip yang sama.
Selain itu, sejak 2015, sebuah tiket telah digantung untuk menambahkan inspeksi ke IntelliJ Idea.
Apa yang harus dilakukan
Solusi paling sederhana adalah dengan bodohnya tidak menggunakan metode ini, karena menurut nama, perilaku mereka, menurut saya, sangat tidak jelas.
Pilihan yang sedikit lebih baik adalah memanggil map(identity)
.
identity
, jika ada yang tidak tahu, ini adalah fungsi dari perpustakaan standar yang hanya mengembalikan argumen inputnya. Dalam hal ini, pekerjaan utama dilakukan dengan metode map
, yang secara eksplisit membuat Map
normal. Mari kita periksa untuk berjaga-jaga:
val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo).map(identity) println(s"resultMapValues: $resultMapValues") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}") println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues")
Pada output yang kita dapatkan
resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) simple assert for resultMapValues: true resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608))
Semua baik-baik saja :)
Jika Anda masih ingin meninggalkan kemalasan, lebih baik mengubah kode sehingga jelas. Anda bisa membuat kelas implisit dengan metode wrapper untuk mapValues
dan filterKeys
, yang memberikan nama baru yang bisa dimengerti oleh mereka. Atau gunakan secara eksplisit .view
dan bekerja dengan iterator berpasangan.
Selain itu, ada baiknya menambahkan inspeksi ke lingkungan pengembangan / aturan dalam penganalisa statis / di tempat lain, yang memperingatkan tentang penggunaan metode ini. Karena lebih baik menghabiskan sedikit waktu sekarang daripada menginjak menyapu dan menyapu konsekuensinya untuk waktu yang lama kemudian.
Bagaimana lagi Anda bisa menginjak menyapu dan bagaimana kita menginjaknya
Selain kasus dengan ketergantungan pada kondisi eksternal, yang kami amati dalam contoh di atas, ada opsi lain.
Misalnya, nilai yang dapat berubah (perhatikan, di sini jika dilihat sekilas semuanya "tidak berubah"):
val someMap1 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) val someMap2 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) def increment(value: AtomicInteger): Int = value.incrementAndGet() val resultMap: Map[String, Int] = someMap1.map { case (k, v) => k -> increment(v) } val resultMapValues: Map[String, Int] = someMap2.mapValues(increment) println(s"resultMap (1): $resultMap") println(s"resultMapValues (1): $resultMapValues") println(s"resultMap (2): $resultMap") println(s"resultMapValues (2): $resultMapValues")
Kode ini akan menghasilkan hasil ini:
resultMap (1): Map(key1 -> 1, key2 -> 1) resultMapValues (1): Map(key1 -> 1, key2 -> 1) resultMap (2): Map(key1 -> 1, key2 -> 1) resultMapValues (2): Map(key1 -> 2, key2 -> 2)
Ketika saya mengakses someMap2
lagi, someMap2
mendapat hasil yang lucu.
Masalah-masalah yang mungkin timbul ketika nilai-nilai mapValues
dan filterKeys
digunakan secara mapValues
- mapValues
dapat ditambahkan ke degradasi kinerja, peningkatan konsumsi memori dan / atau peningkatan beban pada GC, tetapi ini lebih tergantung pada kasus-kasus tertentu dan mungkin tidak begitu kritis.
Anda juga harus menambahkan metode toSeq
dari iterator ke celengan rake yang serupa, yang mengembalikan Stream
malas.
Kami mapValues
tidak sengaja. Itu digunakan dalam metode yang, menggunakan refleksi, menciptakan satu set penangan dari konfigurasi: kunci adalah pengidentifikasi penangan, dan nilai adalah pengaturan mereka, yang kemudian dikonversi ke penangan sendiri (contoh kelas dibuat). Karena handler hanya terdiri dari fungsi murni, semuanya bekerja tanpa masalah, bahkan terasa tidak mempengaruhi kinerja (setelah mengambil penggaruk, kami melakukan pengukuran).
Tetapi begitu saya harus membuat semafor di salah satu penangan agar hanya satu penangan yang melakukan fungsi berat, yang hasilnya disimpan dalam cache dan digunakan oleh penangan lain. Dan kemudian masalah dimulai pada lingkungan pengujian - kode yang valid yang berfungsi dengan baik secara lokal mulai macet karena masalah dengan semaphore. Tentu saja, pemikiran pertama dengan ketidakmampuan fungsi baru adalah bahwa masalahnya terkait dengan itu sendiri. Kami memutar-mutar dengan ini untuk waktu yang lama sampai kami sampai pada kesimpulan "beberapa permainan, mengapa contoh berbeda dari penangan digunakan?" dan itu hanya pada jejak tumpukan yang mereka temukan kami untuk muncul mapValues
.
Jika Anda bekerja dengan Apache Spark, maka Anda dapat menemukan masalah yang sama ketika Anda tiba-tiba menemukan bahwa untuk beberapa bagian dasar kode dengan .mapValues
Anda dapat menangkap
java.io.NotSerializableException: scala.collection.immutable.MapLike$$anon$2
https://stackoverflow.com/questions/32900862/map-can-not-be-serializable-in-scala
https://issues.scala-lang.org/browse/SI-7005
Namun map(identity)
memecahkan masalah, dan biasanya tidak ada motivasi / waktu untuk menggali lebih dalam.
Kesimpulan
Kesalahan dapat mengintai di tempat-tempat yang paling tidak terduga - bahkan dalam metode yang tampaknya 100% jelas. Secara khusus, masalah ini, menurut pendapat saya, terkait dengan nama metode yang buruk dan tipe pengembalian yang tidak cukup ketat.
Tentu saja, penting untuk mempelajari dokumentasi untuk semua metode yang digunakan di perpustakaan standar, tetapi tidak selalu jelas dan, terus terang, tidak selalu ada motivasi yang cukup untuk membaca tentang "hal-hal yang jelas".
Komputasi malas saja adalah lelucon keren, dan artikel itu sama sekali tidak mendorong mereka untuk meninggalkannya. Namun, ketika kemalasan tidak jelas - ini dapat menyebabkan masalah.
Dalam keadilan, masalah dengan nilai mapValues
sudah muncul di Habré dalam terjemahan, tetapi secara pribadi, artikel yang sangat buruk saya masukkan ke dalam kepala saya, karena ada banyak hal yang sudah diketahui / mendasar dan tidak sepenuhnya jelas apa bahayanya menggunakan fungsi-fungsi ini:
Metode filterKeys membungkus tabel sumber tanpa menyalin elemen apa pun. Tidak ada yang salah dengan itu, tetapi Anda hampir tidak mengharapkan perilaku ini dari filterKeys
Artinya, ada komentar tentang perilaku tak terduga, dan bahwa pada saat yang sama Anda juga bisa menginjak menyapu sedikit, tampaknya, itu dianggap tidak mungkin.
→ Semua kode dari artikel ada di intisari ini