Tidak ilmiah tentang monad

Halo semuanya.

Setelah empat tahun pemrograman pada Scala, pemahaman saya tentang monad akhirnya berkembang ke titik di mana Anda dapat menjelaskannya kepada orang lain tanpa mengacu pada teori kategori dan monad klasik - itu hanya monoid dalam kategori endofunders , yang membuat takut programmer tidak lebih buruk daripada kecoak dichlorvos.

Contoh kode akan ditulis di Kotlin, sebagai itu cukup populer, dan pada saat yang sama cukup fungsional (dalam kedua pengertian kata).

Mari kita mulai dengan konsep functor , ini dia:

interface Functor<A> 

Apa maknanya? Functor adalah abstraksi dari perhitungan arbitrer yang mengembalikan hasil tipe A. Kita mengabaikan cara membuat functor baru, dan, yang paling penting, bagaimana menghitung nilainya A. Secara khusus, fungsi dapat bersembunyi di balik antarmuka functor dengan sejumlah argumen sewenang-wenang, dan belum tentu fungsi murni.

Contoh implementasi functor:

  • konstan
  • berfungsi dengan sejumlah argumen arbitrer yang mengembalikan hasil tipe A
  • state pseudo-random generator (Acak)
  • generator nomor acak perangkat keras
  • membaca objek dari disk atau dari jaringan
  • perhitungan asinkron - panggilan balik dilewatkan ke implementasi functor, yang akan dipanggil beberapa waktu kemudian

Semua contoh ini, kecuali untuk konstanta, memiliki satu properti penting - mereka malas, mis. perhitungan itu sendiri tidak terjadi ketika functor dibuat, tetapi ketika dihitung.

Antarmuka functor tidak memungkinkan mendapatkan nilai tipe A dari Functor<A> , atau membuat Functor<A> baru Functor<A> dari nilai tipe A Tetapi bahkan dengan pembatasan seperti itu, functor tidak sia-sia - jika untuk beberapa tipe B kita dapat mengkonversi A ke B (dengan kata lain, ada fungsi (a: A) -> B ), maka kita dapat menulis fungsi (f: Functor<A>) -> Functor<B> dan (f: Functor<A>) -> Functor<B> nama map :

 interface Functor<A> { fun <B> map(f: (A) -> B): Functor<B> } 

Berbeda dengan functor itu sendiri, metode peta tidak bisa menjadi fungsi arbitrer:
- map((a) -> a) harus mengembalikan functor yang sama
- map((a) -> f(a)).map((b) -> g(b)) harus identik dengan map(a -> g(f(a))

Sebagai contoh, kami mengimplementasikan functor yang mengembalikan nilai A yang mengandung sejumlah bit acak. Antarmuka kami di Kotlin tidak dapat digunakan dengan mudah (tetapi Anda dapat , jika diinginkan), jadi kami akan menulis metode ekstensi:

 //  - ,     ,   map data class MyRandom<A>( val get: (bits: Int) -> A ) { companion object { val intRandom: MyRandom<Int> = MyRandom { Random.nextBits(it) } val hexRandom: MyRandom<String> = intRandom.map { it.toString(16) } } } //  map   fun <A, B> MyRandom<A>.map(f: (A) -> B): MyRandom<B> = MyRandom(get = {bits -> f(get(bits)) }) fun main(args: Array<String>) { println("random=" + MyRandom.intRandom.get(12)) //  random=1247 println("hexRandom=" + MyRandom.hexRandom.get(12)) //  hexRandom=c25 } 

Contoh lain dari functors dengan map berguna

  • List<A> abadi List<A>
  • MyInputStream<A>
  • Optional<A>

Sekarang kamu bisa pergi ke monads.

Monad adalah functor dengan dua operasi tambahan. Pertama-tama, monad, tidak seperti functor, berisi operasi penciptaan dari sebuah konstanta, operasi ini disebut lift :

 fun <A> lift(value: A): Monad<A> = TODO() 

Operasi kedua disebut flatMap , ini lebih rumit, jadi pertama-tama kami akan memberikan seluruh antarmuka monad kami:

 interface Monad<A> { //   ,  map     - //    flatMap  lift fun <B> map(f: (A) -> B): Monad<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Monad<B>): Monad<B> } fun <A> lift(value: A): Monad<A> = TODO() 

Perbedaan paling penting antara monad dan functor adalah bahwa monad dapat dikombinasikan satu sama lain, menghasilkan monad baru dan abstrak dari bagaimana monad diimplementasikan - apakah itu membaca dari disk, apakah itu menerima parameter tambahan untuk menghitung nilainya, apakah nilai ini ada . Poin penting kedua - monad tidak digabungkan secara paralel, tetapi secara berurutan, meninggalkan kemampuan untuk menambahkan logika tergantung pada hasil dari monad pertama.

Contoh:

 // ,     Int //       -      //           val readInt: Monad<Int> = TODO() // ,      -  fun readBytes(len: Int): Monad<ByteArray> = TODO() // ,     ,    val bytes: Monad<ByteArray> = readInt.flatMap {len -> if (len > 0) readBytes(len) //    -   else lift(ByteArray(0)) //  ,    } 

Namun, dalam contoh ini tidak disebutkan jaringan. Sama baiknya, data dapat dibaca dari file atau dari database. Mereka dapat dibaca secara sinkron atau asinkron, di sini mungkin ada penanganan kesalahan - semuanya tergantung pada implementasi spesifik dari monad, kode itu sendiri akan tetap tidak berubah.

Pada awalnya contohnya lebih sederhana, Opsi monad. Di kotlin, itu tidak benar-benar dibutuhkan, tetapi di Jawa / Scala sangat berguna:

 data class Option<A>(val value: A?) { fun <B> map(f: (A) -> B): Option<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Option<B>): Option<B> = when(value) { null -> Option(null) else -> f(value) } } fun <A> lift(value: A?): Option<A> = Option(value) fun mul(a: Option<Int>, b: Option<Int>): Option<Int> = a.flatMap { a -> b.map { b -> a * b } } fun main(args: Array<String>) { println(mul(Option(4), Option(5)).value) // 20 println(mul(Option(null), Option(5)).value) // null println(mul(Option(4), Option(null)).value) // null println(mul(Option(null), Option(null)).value) // null } 

Sebagai monad pozakovyristy, mari kita selesaikan pekerjaan basis data dengan monad:

 data class DB<A>(val f: (Connection) -> A) { fun <B> map(f: (A) -> B): DB<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> DB<B>): DB<B> = DB { conn -> f(this.f(conn)).f(conn) } } fun <A> lift(value: A): DB<A> = DB { value } fun select(id: Int): DB<String> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun update(value: String): DB<Unit> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun selectThenUpdate(id: Int): DB<Unit> = select(id).flatMap { value -> update(value) } fun executeTransaction(c: Connection): Unit { //  ,     //          val body: DB<Unit> = selectThenUpdate(42) //  ,   select  update body.f(c) c.commit() } 

Apakah lubang kelinci dalam?


Ada berbagai macam monad, tetapi tujuan utamanya adalah untuk mengabstraksi logika bisnis aplikasi dari beberapa perincian perhitungan yang dilakukan:

  • bahwa nilainya mungkin tidak ada: data class Option<A>(value: A?)
  • bahwa perhitungan akan gagal: data class Either<Error, A>(value: Pair<Error?, A?>)
  • bahwa perhitungannya bisa malas: data class Defer<A>(value: () -> A)
  • atau asinkron: java.util.concurrent.CompletableFuture<A>
  • atau memiliki status fungsional: data class State<S, A>(value: (S) -> Pair<S, A>) Status data class State<S, A>(value: (S) -> Pair<S, A>)

Daftar pertanyaan yang belum terjawab:

  • functors aplikatif - tautan perantara antara functors dan monads
  • koleksi seperti monad
  • komposisi monotipe monad - panah gluesi, transformator monadik
  • urutan / lintasan
  • Monad sebagai efek
  • monad dan rekursi, stack overflow, trampolining
  • Penyandian akhir tanpa tag
  • Io monad
  • dan umumnya seluruh kebun binatang monad standar

Apa selanjutnya


arrow-kt.io
typelevel.org/cats/typeclasses.html
wiki.haskell.org/All_About_Monads

Eksperimen saya adalah aplikasi gaya FP lengkap di Scala:
github.com/scf37/fpscala2

PS Saya ingin catatan kecil, ternyata seperti biasa.

Source: https://habr.com/ru/post/id454534/


All Articles