UPDATE: Pertanyaan ini adalah subjek dari seri blog yang sangat panjang, yang dapat Anda baca di Monads - terima kasih atas pertanyaannya!
Dalam hal yang dimengerti oleh programmer OOP (tanpa latar belakang pemrograman fungsional), apa itu monad?
Monad adalah "penguat" jenis yang mematuhi aturan tertentu dan yang memiliki operasi tertentu disediakan .
Pertama, apa yang dimaksud dengan "penguat tipe"? Maksud saya beberapa sistem yang memungkinkan Anda mengambil jenis dan mengubahnya menjadi jenis yang lebih khusus. Sebagai contoh, dalam C # pertimbangkan Nullable<T>
. Ini adalah penguat jenis. Itu memungkinkan Anda mengambil suatu tipe, mengatakan int
, dan menambahkan kemampuan baru ke tipe itu, yaitu, bahwa sekarang ia bisa menjadi nol ketika sebelumnya tidak bisa.
Sebagai contoh kedua, pertimbangkan IEnumerable<T>
. Ini adalah penguat jenis. Ini memungkinkan Anda mengambil suatu jenis, mengatakan,, string
dan menambahkan kemampuan baru ke jenis itu, yaitu, bahwa Anda sekarang dapat membuat urutan string dari sejumlah string tunggal.
Apa "aturan tertentu"? Secara singkat, bahwa ada cara yang masuk akal untuk fungsi pada tipe yang mendasarinya untuk bekerja pada tipe yang diperkuat sehingga mereka mengikuti aturan normal komposisi fungsional. Misalnya, jika Anda memiliki fungsi pada bilangan bulat, katakanlah
int M(int x) { return x + N(x * 2); }
maka fungsi yang sesuai pada Nullable<int>
dapat membuat semua operator dan panggilan di sana bekerja bersama "dengan cara yang sama" yang mereka lakukan sebelumnya.
(Itu sangat kabur dan tidak tepat; Anda meminta penjelasan yang tidak berasumsi tentang pengetahuan komposisi fungsional.)
Apa itu "operasi"?
Ada operasi "unit" (membingungkan kadang-kadang disebut operasi "kembali") yang mengambil nilai dari tipe polos dan menciptakan nilai monadik yang setara. Ini, pada dasarnya, menyediakan cara untuk mengambil nilai dari tipe yang tidak teramplifikasi dan mengubahnya menjadi nilai dari tipe yang diamplifikasi. Ini dapat diimplementasikan sebagai konstruktor dalam bahasa OO.
Ada operasi "bind" yang mengambil nilai monadik dan fungsi yang dapat mengubah nilai, dan mengembalikan nilai monadik baru. Bind adalah operasi utama yang mendefinisikan semantik monad. Ini memungkinkan kita mengubah operasi pada tipe yang tidak teramplifikasi menjadi operasi pada tipe yang diamplifikasi, yang mematuhi aturan komposisi fungsional yang disebutkan sebelumnya.
Sering ada cara untuk mendapatkan kembali tipe yang tidak teramplifikasi dari tipe yang diamplifikasi. Tegasnya operasi ini tidak wajib memiliki monad. (Meskipun perlu jika Anda ingin memiliki comonad . Kami tidak akan mempertimbangkan yang lebih lanjut dalam artikel ini.)
Sekali lagi, ambil Nullable<T>
sebagai contoh. Anda dapat mengubah sebuah int
menjadi Nullable<int>
dengan konstruktor. Kompiler C # menangani "lifting" yang paling dapat dibatalkan untuk Anda, tetapi jika tidak, transformasi pengangkatannya mudah: sebuah operasi, katakanlah,
int M(int x) { whatever }
ditransformasikan menjadi
Nullable<int> M(Nullable<int> x)
{
if (x == null)
return null;
else
return new Nullable<int>(whatever);
}
Dan mengubah Nullable<int>
kembali menjadi int
selesai dengan Value
properti.
Ini adalah transformasi fungsi yang merupakan bit kunci. Perhatikan bagaimana semantik sebenarnya dari operasi yang dapat dibatalkan - bahwa operasi pada null
propagasi null
- ditangkap dalam transformasi. Kita bisa menggeneralisasi ini.
Misalkan Anda memiliki fungsi dari int
hingga int
, seperti aslinya kami M
. Anda dapat dengan mudah menjadikannya sebagai fungsi yang mengambil int
dan mengembalikan a Nullable<int>
karena Anda bisa menjalankan hasilnya melalui konstruktor yang dapat dibatalkan. Sekarang anggaplah Anda memiliki metode tingkat tinggi ini:
static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
if (amplified == null)
return null;
else
return func(amplified.Value);
}
Lihat apa yang bisa Anda lakukan dengan itu? Setiap metode yang mengambil int
dan mengembalikan sebuah int
, atau mengambil int
dan mengembalikan Nullable<int>
sekarang dapat memiliki semantik nullable diterapkan untuk itu .
Selanjutnya: misalkan Anda memiliki dua metode
Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }
dan Anda ingin membuatnya:
Nullable<int> Z(int s) { return X(Y(s)); }
Artinya, Z
adalah komposisi X
dan Y
. Tetapi Anda tidak dapat melakukannya karena X
membutuhkan int
, dan Y
mengembalikan a Nullable<int>
. Tetapi karena Anda memiliki operasi "bind", Anda dapat membuatnya bekerja:
Nullable<int> Z(int s) { return Bind(Y(s), X); }
Operasi mengikat pada monad adalah apa yang membuat komposisi fungsi pada jenis yang diperkuat bekerja. "Aturan" yang saya berikan di atas adalah bahwa monad mempertahankan aturan komposisi fungsi normal; yang menyusun dengan fungsi identitas menghasilkan fungsi asli, komposisi itu asosiatif, dan sebagainya.
Dalam C #, "Bind" disebut "SelectMany". Lihatlah cara kerjanya pada urutan monad. Kita perlu memiliki dua hal: mengubah nilai menjadi urutan dan mengikat operasi pada urutan. Sebagai bonus, kami juga memiliki "mengubah urutan kembali menjadi nilai". Operasi tersebut adalah:
static IEnumerable<T> MakeSequence<T>(T item)
{
yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
// let's just take the first one
foreach(T item in sequence) return item;
throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
foreach(T item in seq)
foreach(T result in func(item))
yield return result;
}
Aturan monad yang dapat dibatalkan adalah "untuk menggabungkan dua fungsi yang menghasilkan nullables bersama, periksa untuk melihat apakah bagian dalam menghasilkan nol; jika itu, menghasilkan nol, jika tidak, maka panggil bagian luar dengan hasilnya". Itu semantik yang diinginkan dari nullable.
Aturan urutan monad adalah "untuk menggabungkan dua fungsi yang menghasilkan urutan bersama, menerapkan fungsi luar untuk setiap elemen yang dihasilkan oleh fungsi bagian dalam, dan kemudian menggabungkan semua urutan yang dihasilkan bersama-sama". Semantik dasar dari monad ditangkap dalam Bind
/ SelectMany
metode; ini adalah metode yang memberitahu Anda apa yang monad benar-benar berarti .
Kita bisa melakukan yang lebih baik lagi. Misalkan Anda memiliki urutan int, dan metode yang mengambil int dan menghasilkan urutan string. Kita dapat menggeneralisasi operasi pengikatan untuk memungkinkan komposisi fungsi yang mengambil dan mengembalikan berbagai jenis yang diamplifikasi, selama input dari satu cocok dengan output dari yang lain:
static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
foreach(T item in seq)
foreach(U result in func(item))
yield return result;
}
Jadi sekarang kita dapat mengatakan "perkuat kumpulan bilangan bulat individual ini menjadi urutan bilangan bulat. Ubah bilangan bulat khusus ini menjadi sekelompok string, diamplifikasi ke urutan string. Sekarang satukan kedua operasi bersama: perkuat kumpulan integer ini ke dalam gabungan dari semua urutan string. " Monads memungkinkan Anda membuat amplifikasi.
Masalah apa yang dipecahkan dan tempat apa yang paling umum digunakan?
Itu agak seperti bertanya "masalah apa yang dipecahkan oleh pola singleton?", Tetapi saya akan mencobanya.
Monads biasanya digunakan untuk menyelesaikan masalah seperti:
- Saya perlu membuat kemampuan baru untuk tipe ini dan masih menggabungkan fungsi lama pada tipe ini untuk menggunakan kemampuan baru.
- Saya perlu menangkap banyak operasi pada jenis dan mewakili operasi tersebut sebagai objek yang dapat dikomposisikan, membangun komposisi yang lebih besar dan lebih besar sampai saya memiliki rangkaian operasi yang tepat yang diwakili, dan kemudian saya harus mulai mendapatkan hasil dari hal tersebut.
- Saya perlu mewakili operasi efek samping secara bersih dalam bahasa yang membenci efek samping
C # menggunakan monads dalam desainnya. Seperti yang telah disebutkan, pola nullable sangat mirip dengan "mungkin monad". LINQ seluruhnya dibangun dari monad; yang SelectMany
metode adalah apa cara kerja semantik komposisi operasi. (Erik Meijer gemar menunjukkan bahwa setiap fungsi LINQ sebenarnya dapat diimplementasikan oleh SelectMany
; segala sesuatu yang lain hanya kenyamanan.)
Untuk memperjelas jenis pemahaman yang saya cari, katakanlah Anda mengubah aplikasi FP yang memiliki monads menjadi aplikasi OOP. Apa yang akan Anda lakukan untuk memindahkan tanggung jawab monad ke dalam aplikasi OOP?
Sebagian besar bahasa OOP tidak memiliki sistem tipe yang cukup kaya untuk mewakili pola monad itu sendiri secara langsung; Anda memerlukan sistem tipe yang mendukung tipe yang tipe lebih tinggi daripada tipe generik. Jadi saya tidak akan mencoba melakukan itu. Sebaliknya, saya akan menerapkan tipe generik yang mewakili masing-masing monad, dan menerapkan metode yang mewakili tiga operasi yang Anda butuhkan: mengubah nilai menjadi nilai yang diperkuat, (mungkin) mengubah nilai yang diperkuat menjadi nilai, dan mengubah fungsi pada nilai yang tidak teramplifikasi menjadi nilai sebuah fungsi pada nilai yang diamplifikasi.
Tempat yang baik untuk memulai adalah bagaimana kami mengimplementasikan LINQ di C #. Pelajari SelectMany
metodenya; itu adalah kunci untuk memahami cara kerja urutan monad di C #. Ini adalah metode yang sangat sederhana, tetapi sangat kuat!
Disarankan, bacaan lebih lanjut:
- Untuk penjelasan yang lebih mendalam dan secara teori logis tentang monad dalam bahasa C #, saya sangat merekomendasikan artikel saya ( Eric Lippert ), rekan sejawat, Wes Dyer mengenai masalah ini. Artikel ini menjelaskan monads kepada saya ketika mereka akhirnya "mengklik" untuk saya.
- Sebuah ilustrasi yang baik mengapa Anda mungkin ingin monad sekitar (menggunakan Haskell di contoh itu) .
- Semacam, "terjemahan" dari artikel sebelumnya ke JavaScript.