Entri
Di
YOW! 2013 salah satu pengembang bahasa Haskell,
prof. Philip Wadler menunjukkan bagaimana monad memungkinkan bahasa fungsional murni untuk melakukan operasi penting pada dasarnya, seperti input-output dan penanganan pengecualian. Tidak mengherankan, minat audiens pada topik ini telah menghasilkan pertumbuhan eksplosif dalam publikasi tentang monad di Internet. Sayangnya, sebagian besar publikasi ini menggunakan contoh-contoh yang ditulis dalam bahasa fungsional, menyiratkan bahwa pendatang baru untuk pemrograman fungsional ingin belajar tentang monad. Tetapi monad tidak spesifik untuk Haskell atau bahasa fungsional, dan mungkin diilustrasikan dengan contoh-contoh dalam bahasa pemrograman imperatif. Ini adalah tujuan dari panduan ini.
Bagaimana panduan ini berbeda dari yang lain? Kami akan mencoba membuka monad dalam waktu tidak lebih dari 15 menit hanya dengan menggunakan intuisi dan beberapa contoh dasar kode Python. Oleh karena itu, kita tidak akan mulai berteori dan mempelajari filosofi, berbicara tentang
burrito ,
pakaian luar angkasa ,
meja dan endofunctor.
Contoh motivasi
Kami akan mempertimbangkan tiga masalah terkait komposisi fungsi. Kami akan menyelesaikannya dalam dua cara: imperatif biasa dan menggunakan monad. Kemudian kami membandingkan pendekatan yang berbeda.
1. Pencatatan
Misalkan kita memiliki tiga fungsi unary:
f1
,
f2
dan
f3
, yang mengambil angka dan mengembalikannya masing-masing meningkat 1, 2, dan 3. Setiap fungsi juga menghasilkan pesan, yang merupakan laporan tentang operasi yang selesai.
def f1(x): return (x + 1, str(x) + "+1") def f2(x): return (x + 2, str(x) + "+2") def f3(x): return (x + 3, str(x) + "+3")
Kami ingin rantai mereka bersama-sama untuk memproses parameter
x
, dengan kata lain, kami ingin menghitung
x+1+2+3
. Selain itu, kita perlu mendapatkan penjelasan yang bisa dibaca manusia tentang apa yang masing-masing fungsi lakukan.
Kita dapat mencapai hasil yang kita butuhkan dengan cara berikut:
log = "Ops:" res, log1 = f1(x) log += log1 + ";" res, log2 = f2(res) log += log2 + ";" res, log3 = f3(res) log += log3 + ";" print(res, log)
Solusi ini tidak ideal, karena terdiri dari sejumlah besar middleware monoton. Jika kami ingin menambahkan fungsi baru ke rantai kami, kami akan dipaksa untuk mengulangi kode tautan ini. Selain itu, manipulasi dengan variabel
res
dan
log
mengganggu pembacaan kode, sehingga sulit untuk mengikuti logika utama program.
Idealnya, sebuah program harus terlihat seperti rangkaian fungsi sederhana, seperti
f3(f2(f1(x)))
. Sayangnya, tipe data yang dikembalikan oleh
f1
dan
f2
tidak cocok dengan tipe parameter
f2
dan
f3
. Tapi kita bisa menambahkan fungsi baru ke rantai:
def unit(x): return (x, "Ops:") def bind(t, f): res = f(t[0]) return (res[0], t[1] + res[1] + ";")
Sekarang kita bisa menyelesaikan masalah sebagai berikut:
print(bind(bind(bind(unit(x), f1), f2), f3))
Diagram berikut menunjukkan proses komputasi yang terjadi pada
x=0
. Di sini
v1
,
v2
dan
v3
adalah nilai yang diperoleh sebagai hasil dari panggilan ke
unit
dan
bind
.

Fungsi
unit
mengubah parameter input
x
menjadi tupel angka dan string. Fungsi
bind
memanggil fungsi yang diteruskan kepadanya sebagai parameter dan mengakumulasikan hasilnya dalam variabel perantara
t
.
Kami dapat menghindari pengulangan middleware dengan menempatkannya di fungsi
bind
. Sekarang, jika kita mendapatkan fungsi
f4
, kita cukup memasukkannya dalam rantai:
bind(f4, bind(f3, ... ))
Dan kita tidak perlu melakukan perubahan lain.
2. Daftar nilai antara
Kami juga akan memulai contoh ini dengan fungsi unary sederhana.
def f1(x): return x + 1 def f2(x): return x + 2 def f3(x): return x + 3
Seperti pada contoh sebelumnya, kita perlu membuat fungsi-fungsi ini untuk menghitung
x+1+2+3
. Kita juga perlu mendapatkan daftar semua nilai yang diperoleh sebagai hasil kerja fungsi kita, yaitu,
x
,
x+1
,
x+1+2
dan
x+1+2+3
.
Tidak seperti contoh sebelumnya, fungsi kami dapat dikompilasi, yaitu, jenis parameter inputnya bertepatan dengan jenis hasilnya. Oleh karena itu, rantai sederhana
f3(f2(f1(x)))
akan mengembalikan hasil akhir. Namun dalam kasus ini, kami kehilangan nilai perantara.
Mari kita selesaikan masalah "langsung":
lst = [x] res = f1(x) lst.append(res) res = f2(res) lst.append(res) res = f3(res) lst.append(res) print(res, lst)
Sayangnya, solusi ini juga mengandung banyak middleware. Dan jika kita memutuskan untuk menambahkan
f4
, kita harus mengulangi kode ini lagi untuk mendapatkan daftar nilai perantara yang benar.
Oleh karena itu, kami menambahkan dua fungsi tambahan, seperti pada contoh sebelumnya:
def unit(x): return (x, [x]) def bind(t, f): res = f(t[0]) return (res, t[1] + [res])
Sekarang kami menulis ulang program sebagai rantai panggilan:
print(bind(bind(bind(unit(x), f1), f2), f3))
Diagram berikut menunjukkan proses komputasi yang terjadi pada
x=0
. Sekali lagi,
v1
,
v2
dan
v3
menunjukkan nilai yang diperoleh dari panggilan
unit
dan
bind
.

3. Nilai kosong
Mari kita coba memberikan contoh yang lebih menarik, kali ini dengan kelas dan objek. Misalkan kita memiliki kelas
Employee
dengan dua metode:
class Employee: def get_boss(self):
Setiap objek dari kelas
Employee
memiliki manajer (objek lain dari kelas
Employee
) dan gaji, yang diakses melalui metode yang tepat. Kedua metode juga dapat mengembalikan
None
(karyawan tidak memiliki manajer, gajinya tidak diketahui).
Dalam contoh ini, kami akan membuat program yang menunjukkan gaji pimpinan karyawan ini. Jika manajer tidak ditemukan, atau gajinya tidak dapat ditentukan, maka program tidak akan mengembalikan apa
None
.
Idealnya, kita perlu menulis sesuatu seperti
print(john.get_boss().get_wage())
Tetapi dalam kasus ini, jika salah satu metode mengembalikan
None
, program kami akan berakhir dengan kesalahan.
Cara yang jelas untuk menangani situasi ini mungkin terlihat seperti ini:
result = None if john is not None and john.get_boss() is not None and john.get_boss().get_wage() is not None: result = john.get_boss().get_wage() print(result)
Dalam hal ini, kami mengizinkan panggilan tambahan ke metode
get_boss
dan
get_wage
. Jika metode ini cukup berat (misalnya, mengakses database), solusi kami tidak akan berhasil. Karena itu, kami mengubahnya:
result = None if john is not None: boss = john.get_boss() if boss is not None: wage = boss.get_wage() if wage is not None: result = wage print(result)
Kode ini optimal dalam hal perhitungan, tetapi kurang dibaca karena tiga
if
bersarang. Karena itu, kami akan mencoba menggunakan trik yang sama seperti pada contoh sebelumnya. Tentukan dua fungsi:
def unit(e): return e def bind(e, f): return None if e is None else f(e)
Dan sekarang kita dapat menempatkan seluruh solusi dalam satu baris.
print(bind(bind(unit(john), Employee.get_boss), Employee.get_wage))
Seperti yang mungkin sudah Anda perhatikan, dalam hal ini kami tidak harus menulis fungsi
unit
: hanya mengembalikan parameter input. Tetapi kita akan meninggalkannya sehingga akan lebih mudah bagi kita untuk menggeneralisasi pengalaman kita.
Perhatikan juga bahwa dalam Python kita dapat menggunakan metode sebagai fungsi, yang memungkinkan kita untuk menulis
Employee.get_boss(john)
alih-alih
john.get_boss()
.
Diagram berikut menunjukkan proses perhitungan ketika John tidak memiliki pemimpin, yaitu
john.get_boss()
mengembalikan
None
.

Kesimpulan
Misalkan kita ingin menggabungkan fungsi dengan tipe yang sama
f1
,
f2
,
…
,
fn
. Jika parameter inputnya sama dengan hasilnya, kita dapat menggunakan rantai sederhana dari bentuk
fn(… f2(f1(x)) …)
. Diagram berikut menunjukkan proses perhitungan umum dengan hasil antara, dilambangkan sebagai
v1
,
v2
,
…
,
vn
.

Seringkali pendekatan ini tidak berlaku. Jenis nilai input dan hasil fungsi dapat bervariasi, seperti pada contoh pertama. Atau fungsi dapat dikomposisikan, tetapi kami ingin memasukkan logika tambahan di antara panggilan, seperti dalam contoh 2 dan 3 kami memasukkan agregasi nilai antara dan masing-masing untuk memeriksa nilai kosong.
1. Keputusan penting
Dalam semua contoh, kami pertama kali menggunakan pendekatan yang paling mudah, yang dapat diwakili oleh diagram berikut:

Sebelum memanggil
f1
kami melakukan inisialisasi. Dalam contoh pertama, kami menginisialisasi variabel untuk menyimpan log umum, yang kedua untuk daftar nilai-nilai perantara. Setelah itu, kami menyelingi panggilan fungsi dengan kode penghubung tertentu: kami menghitung nilai agregat, memeriksa hasilnya untuk
None
.
2. Monad
Seperti yang kita lihat dalam contoh, keputusan imperatif selalu menderita dari kata kerja, pengulangan, dan logika yang membingungkan. Untuk mendapatkan kode yang lebih elegan, kami menggunakan pola desain tertentu, yang dengannya kami membuat dua fungsi:
unit
dan
bind
. Template ini disebut
monad . Fungsi
bind
berisi middleware sementara
unit
mengimplementasikan inisialisasi. Ini memungkinkan kami untuk menyederhanakan solusi akhir menjadi satu baris:
bind(bind( ... bind(bind(unit(x), f1), f2) ... fn-1), fn)
Proses perhitungan dapat diwakili oleh diagram:

Panggilan ke
unit(x)
menghasilkan nilai awal
v1
. Kemudian
bind(v1, f1)
menghasilkan nilai menengah baru
v2
, yang digunakan dalam panggilan berikutnya untuk
bind(v2, f2)
. Proses ini berlanjut sampai hasil akhir diperoleh. Dengan mendefinisikan berbagai
unit
dan
bind
dalam kerangka templat ini, kita dapat menggabungkan berbagai fungsi dalam satu rantai perhitungan. Perpustakaan Monad (
misalnya, PyMonad atau OSlash - sekitar Terjemahan )
. Biasanya berisi monad yang siap digunakan (pasangan
unit
dan fungsi
bind
) untuk menerapkan komposisi fungsi tertentu.
Untuk fungsi rantai, nilai yang dikembalikan oleh
unit
dan
bind
harus dari jenis yang sama dengan parameter input dari
bind
. Tipe ini disebut
monadik . Dalam hal diagram di atas, tipe variabel
v1
,
v2
,
…
,
vn
harus merupakan tipe monadik.
Akhirnya, pertimbangkan bagaimana Anda dapat meningkatkan kode yang ditulis menggunakan monads. Jelas, panggilan
bind
berulang-ulang terlihat tidak bagus. Untuk menghindarinya, tentukan fungsi eksternal lain:
def pipeline(e, *functions): for f in functions: e = bind(e, f) return e
Sekarang sebagai gantinya
bind(bind(bind(bind(unit(x), f1), f2), f3), f4)
kita bisa menggunakan singkatan berikut:
pipeline(unit(x), f1, f2, f3, f4)
Kesimpulan
Monads adalah pola desain sederhana dan kuat yang digunakan untuk menyusun fungsi. Dalam bahasa pemrograman deklaratif, ini membantu untuk mengimplementasikan mekanisme imperatif seperti logging atau input / output. Dalam bahasa imperatif
ini membantu untuk menggeneralisasi dan mempersingkat kode yang menghubungkan serangkaian panggilan fungsi dengan tipe yang sama.
Artikel ini hanya memberikan pemahaman dangkal dan intuitif tentang monad. Anda dapat mengetahui lebih lanjut dengan menghubungi sumber-sumber berikut:
- Wikipedia
- Monads dengan Python (dengan sintaks yang bagus!)
- Kronologi tutorial Monad