Monads dalam 15 menit

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): # Return the employee's boss def get_wage(self): # Compute the wage 

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:

  1. Wikipedia
  2. Monads dengan Python (dengan sintaks yang bagus!)
  3. Kronologi tutorial Monad

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


All Articles