"Efek aljabar" dalam bahasa manusia

Komentar Penerjemah: Ini adalah terjemahan dari artikel hebat oleh Dan Abramov, kontributor React. Contoh-contohnya ditulis untuk JS, tetapi akan jelas bagi pengembang dalam bahasa apa pun. Idenya umum untuk semua.

Pernahkah Anda mendengar tentang efek aljabar?


Upaya pertama saya untuk mengetahui siapa mereka dan mengapa mereka harus membuat saya bersemangat tidak berhasil. Saya menemukan beberapa PDF , tetapi lebih membingungkan saya. (Untuk beberapa alasan, saya tertidur ketika membaca artikel akademik.)


Tetapi kolega saya, Sebastian, terus menyebut mereka model mental dari beberapa hal yang kami lakukan dalam Bereaksi. (Sebastian bekerja di tim React dan mengajukan banyak ide, termasuk Hooks dan Suspense.) Pada titik tertentu, itu menjadi meme lokal di tim React, dan banyak dari percakapan kami berakhir dengan yang berikut:



Ternyata efek aljabar adalah konsep yang keren, dan tidak seseram yang saya rasakan pada awalnya setelah membaca PDF ini. Jika Anda hanya menggunakan Bereaksi, Anda tidak perlu tahu apa pun tentang mereka, tetapi jika Anda, seperti saya, tertarik, baca terus.


(Penafian: Saya bukan seorang peneliti di bidang bahasa pemrograman dan mungkin telah mengacaukan sesuatu dalam penjelasan saya. Jadi beri tahu saya jika saya salah!)


Ini masih awal produksi


Efek aljabar saat ini merupakan konsep eksperimental dari bidang studi bahasa pemrograman. Ini berarti bahwa tidak seperti if , for atau bahkan async/await ekspresi, Anda kemungkinan besar tidak akan dapat menggunakannya sekarang dalam produksi. Mereka didukung oleh hanya beberapa bahasa yang diciptakan khusus untuk mempelajari ide ini. Ada kemajuan dalam implementasi mereka di OCaml, yang ... masih berlangsung . Dengan kata lain, perhatikan, tetapi jangan menyentuh dengan tangan Anda.


Kenapa itu harus mengganggu saya?


Bayangkan Anda menulis kode menggunakan goto , dan seseorang memberi tahu Anda tentang keberadaan if dan for constructs. Atau mungkin Anda terperosok dalam neraka panggilan balik dan seseorang menunjukkan kepada Anda async/await . Cukup keren, bukan?


Jika Anda adalah tipe orang yang suka mempelajari inovasi pemrograman beberapa tahun sebelum menjadi modis, mungkin ini saatnya untuk tertarik pada efek aljabar. Meski tidak perlu. Ini adalah cara berbicara tentang async/await pada tahun 1999.


Nah, apa efeknya?


Nama itu mungkin sedikit membingungkan, tetapi idenya sederhana. Jika Anda terbiasa dengan blok try/catch , Anda akan dengan cepat memahami efek aljabar.


Mari kita ingat try/catch . Katakanlah Anda memiliki fungsi yang melempar pengecualian. Mungkin ada beberapa panggilan bersarang di antara itu dan catch :


 function getName(user) { let name = user.name; if (name === null) { throw new Error('  '); } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } catch (err) { console.log(",   : ", err); } 

Kami melempar pengecualian di dalam getName , tetapi "muncul" melalui makeFriends ke catch terdekat. Ini adalah properti utama try/catch . Kode perantara tidak diperlukan untuk peduli dengan penanganan kesalahan.


Tidak seperti kode kesalahan dalam bahasa seperti C, saat menggunakan try/catch Anda tidak harus melewati kesalahan secara manual melalui setiap tingkat perantara untuk menangani kesalahan di tingkat atas. Pengecualian muncul secara otomatis.


Apa hubungannya ini dengan efek aljabar?


Dalam contoh di atas, segera setelah kami melihat kesalahan, kami tidak akan dapat melanjutkan menjalankan program. Ketika kita menemukan diri kita dalam catch , eksekusi program normal akan berhenti.


Semua sudah berakhir. Sudah terlambat. Yang terbaik yang bisa kita lakukan adalah pulih dari kegagalan dan mungkin entah bagaimana mengulangi apa yang kita lakukan, tetapi secara ajaib kita tidak bisa "kembali" ke tempat kita berada dan melakukan sesuatu yang lain. Dan dengan efek aljabar, kita bisa.


Ini adalah contoh yang ditulis dalam dialek JavaScript hipotetis (sebut saja ES2025 untuk bersenang-senang), yang memungkinkan kami untuk terus bekerja setelah user.name hilang:


 function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } } 

(Saya meminta maaf kepada semua pembaca dari 2025 yang mencari "ES2025" di Internet dan masuk ke dalam artikel ini. Jika pada saat itu efek aljabar akan menjadi bagian dari JavaScript, saya akan dengan senang hati memperbarui artikel!)


Alih-alih throw kami menggunakan perform kata kunci hipotetis. Demikian pula, alih-alih try/catch kami menggunakan try/handle hipotetis. Sintaks yang tepat tidak masalah di sini - saya baru saja menemukan sesuatu untuk mengilustrasikan ide tersebut.


Jadi apa yang terjadi di sini? Mari kita lihat lebih dekat.


Alih-alih melempar kesalahan, kami melakukan efeknya . Sama seperti kita dapat membuang objek apa pun, di sini kita dapat memberikan beberapa nilai untuk diproses . Dalam contoh ini, saya meneruskan string, tetapi bisa berupa objek atau tipe data lainnya:


 function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } 

Saat kami melempar pengecualian, mesin mencari pawang try/catch terdekat di tumpukan panggilan. Demikian pula, ketika kita menjalankan efek , mesin akan mencari try/handle efek try/handle terdekat di atas tumpukan:


 try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } } 

Efek ini memungkinkan kita untuk memutuskan bagaimana menangani situasi ketika nama tidak ditentukan. Baru di sini (dibandingkan dengan pengecualian) adalah resume with hipotetis resume with :


 try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } } 

Ini adalah sesuatu yang tidak dapat Anda lakukan dengan try/catch . Ini memungkinkan kita untuk kembali ke tempat kita melakukan efek dan melewatkan sesuatu dari pawang . : -O


 function getName(user) { let name = user.name; if (name === null) { // 1.     name = perform 'ask_name'; // 4. ...     (name   ' ') } return name; } // ... try { makeFriends(arya, gendry); } handle(effect) { // 2.    ( try/catch)  (effect === 'ask_name') { // 3. ,      (    try/catch!) resume with ' '; } } 

Butuh sedikit waktu untuk merasa nyaman, tetapi secara konseptual ini tidak jauh berbeda dari try/catch dengan pengembalian.


Perhatikan, bagaimanapun, bahwa efek aljabar adalah alat yang jauh lebih kuat daripada sekadar try/catch . Pemulihan kesalahan hanyalah salah satu dari banyak kasus penggunaan yang mungkin. Saya mulai dengan contoh ini hanya karena paling mudah bagi saya untuk mengerti.



Fungsi tidak memiliki warna


Efek aljabar memiliki implikasi yang menarik untuk kode asinkron.


Dalam bahasa dengan async/await fungsi biasanya memiliki "warna" ( Rusia ). Misalnya, dalam JavaScript, kita tidak bisa hanya membuat getName asynchronous tanpa menginfeksi makeFriends dan fungsi pemanggilannya dengan async. Ini bisa sangat menyakitkan jika bagian dari kode terkadang perlu sinkron dan terkadang tidak sinkron.



 //       ... async getName(user) { // ... } //       ... async function makeFriends(user1, user2) { user1.friendNames.add(await getName(user2)); user2.friendNames.add(await getName(user1)); } //   ... async getName(user) { // ... } 

Generator JavaScript bekerja dengan cara yang sama : jika Anda bekerja dengan generator, maka semua kode perantara juga harus tahu tentang generator.


Nah, apa hubungannya dengan itu?


Untuk sesaat, mari kita lupakan tentang async / tunggu dan kembali ke contoh kita:


 function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } } 

Bagaimana jika penangan efek kami tidak dapat mengembalikan "nama cadangan" secara serempak? Bagaimana jika kita ingin mendapatkannya dari database?


Ternyata kami dapat memanggil resume with sinkron dari penangan efek kami tanpa membuat perubahan apa pun untuk getName atau makeFriends :


 function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { setTimeout(() => { resume with ' '; }, 1000); } } 

Dalam contoh ini, kami memanggil resume with hanya satu detik kemudian. Anda dapat mempertimbangkan resume with panggilan balik, yang hanya dapat Anda panggil sekali. (Anda juga dapat memamerkan kepada teman-teman dengan menyebut hal ini " kelanjutan terbatas satu kali" (istilah kelanjutan terbatas belum menerima terjemahan yang stabil ke dalam bahasa Rusia - sekitar Terjemahan.).)


Sekarang mekanisme efek aljabar harus sedikit lebih jelas. Ketika kami melempar kesalahan, mesin JavaScript memutar tumpukan dengan menghancurkan variabel lokal dalam proses. Namun, ketika kita mengeksekusi efeknya, mesin hipotetis kita membuat panggilan balik (sebenarnya "bingkai lanjutan", kira-kira. Terjemahan.) Dengan sisa fungsi kita, dan resume with akan memanggilnya.


Sekali lagi, pengingat: sintaks spesifik dan kata kunci spesifik seluruhnya diciptakan hanya untuk artikel ini. Intinya bukan di dalamnya, tetapi di mekanik.



Catatan Kebersihan


Perlu dicatat bahwa efek aljabar muncul sebagai hasil dari studi pemrograman fungsional. Beberapa masalah yang mereka selesaikan hanya unik untuk pemrograman fungsional. Misalnya, dalam bahasa yang tidak mengizinkan efek samping sewenang-wenang (seperti Haskell), Anda harus menggunakan konsep seperti monad untuk menyeret efek melalui program Anda. Jika Anda pernah membaca tutorial monad, maka Anda tahu bahwa itu bisa sulit dimengerti. Efek aljabar membantu untuk melakukan sesuatu yang serupa dengan sedikit usaha.


Itulah mengapa sebagian besar diskusi tentang efek aljabar sama sekali tidak bisa dipahami oleh saya. (Saya tidak tahu Haskell dan "teman-teman" -nya.) Namun, saya berpikir bahwa bahkan dalam bahasa yang tidak bersih seperti JavaScript, efek aljabar dapat menjadi alat yang sangat kuat untuk memisahkan "apa" dari "bagaimana" dalam "bagaimana" dalam kode Anda.


Mereka memungkinkan Anda untuk menulis kode yang menggambarkan apa yang Anda lakukan:


 function enumerateFiles(dir) { const contents = perform OpenDirectory(dir); perform Log('Enumerating files in ', dir); for (let file of contents.files) { perform HandleFile(file); } perform Log('Enumerating subdirectories in ', dir); for (let directory of contents.dir) { //           enumerateFiles(directory); } perform Log('Done'); } 

Dan kemudian bungkus dengan sesuatu yang menggambarkan "bagaimana" Anda melakukannya:


 let files = []; try { enumerateFiles('C:\\'); } handle(effect) { if (effect instanceof Log) { myLoggingLibrary.log(effect.message); resume; } else if (effect instanceof OpenDirectory) { myFileSystemImpl.openDir(effect.dirName, (contents) => { resume with contents; }); } else if (effect instanceof HandleFile) { files.push(effect.fileName); resume; } } //  `files`     

Yang berarti bahwa bagian-bagian ini dapat menjadi perpustakaan:


 import { withMyLoggingLibrary } from 'my-log'; import { withMyFileSystem } from 'my-fs'; function ourProgram() { enumerateFiles('C:\\'); } withMyLoggingLibrary(() => { withMyFileSystem(() => { ourProgram(); }); }); 

Tidak seperti async / menunggu atau generator, efek aljabar tidak memerlukan komplikasi fungsi "perantara". Panggilan kami ke enumerateFiles mungkin jauh di dalam program kami, tetapi selama suatu tempat di atas ada penangan efek untuk setiap efek yang dapat dieksekusi, kode kami akan terus bekerja.


Penangan efek memungkinkan kita untuk memisahkan logika program dari implementasi spesifik efeknya tanpa tarian dan kode boilerplate yang tidak perlu. Sebagai contoh, kita dapat mendefinisikan kembali perilaku dalam pengujian untuk menggunakan sistem file palsu dan melakukan snapshot log alih-alih menampilkannya di konsol:


 import { withFakeFileSystem } from 'fake-fs'; function withLogSnapshot(fn) { let logs = []; try { fn(); } handle(effect) { if (effect instanceof Log) { logs.push(effect.message); resume; } } // Snapshot  . expect(logs).toMatchSnapshot(); } test('my program', () => { const fakeFiles = [ /* ... */ ]; withFakeFileSystem(fakeFiles, () => { withLogSnapshot(() => { ourProgram(); }); }); }); 

Karena fungsi tidak memiliki "warna" (kode perantara tidak harus tahu tentang efek), dan penangan efek dapat dikomposisikan (mereka dapat disarangkan), Anda dapat membuat abstraksi yang sangat ekspresif dengannya.



Jenis Catatan


Karena efek aljabar berasal dari bahasa yang diketik secara statis, sebagian besar perdebatan tentang mereka berfokus pada cara mengekspresikannya dalam jenis. Ini tidak diragukan lagi penting, tetapi juga dapat memperumit pemahaman konsep. Itu sebabnya artikel ini tidak berbicara tentang tipe sama sekali. Namun, saya harus mencatat bahwa biasanya fakta bahwa suatu fungsi dapat melakukan efek akan dikodekan dalam tanda tangan jenisnya. Dengan demikian, Anda akan dilindungi dari situasi ketika efek yang tidak terduga dilakukan, atau Anda tidak dapat melacak dari mana mereka berasal.


Di sini Anda dapat mengatakan bahwa efek aljabar teknis "memberi warna" untuk fungsi dalam bahasa yang diketik secara statis, karena efek adalah bagian dari tanda tangan jenis. Memang benar. Namun, memperbaiki anotasi jenis untuk fungsi antara untuk memasukkan efek baru tidak dengan sendirinya merupakan perubahan semantik - tidak seperti menambahkan async atau mengubah fungsi menjadi generator. Ketik inferensi juga dapat membantu menghindari kebutuhan untuk perubahan cascading. Perbedaan penting adalah bahwa Anda dapat "menekan" efek dengan memasukkan rintisan kosong atau implementasi sementara (misalnya, panggilan sinkronisasi untuk efek asinkron), yang, jika perlu, memungkinkan Anda untuk mencegah efeknya pada kode eksternal - atau mengubahnya menjadi efek lain.



Apakah saya perlu efek aljabar dalam JavaScript?


Jujur saja, saya tidak tahu. Mereka sangat kuat, dan dapat dikatakan bahwa mereka terlalu kuat untuk bahasa seperti JavaScript.


Saya pikir mereka bisa sangat berguna untuk bahasa di mana mutabilitas jarang terjadi dan di mana pustaka standar sepenuhnya mendukung efek. Jika Anda pertama kali melakukan perform Timeout(1000), perform Fetch('http://google.com') , dan perform ReadFile('file.txt') , dan bahasa Anda memiliki "pencocokan pola" dan pengetikan statis untuk efek, kemudian ini bisa menjadi lingkungan pemrograman yang sangat bagus.


Mungkin bahasa ini bahkan akan dikompilasi dalam JavaScript!



Apa hubungannya ini dengan Bereaksi?


Tidak terlalu besar. Anda bahkan dapat mengatakan bahwa saya menarik burung hantu di bola dunia.


Jika Anda menonton ceramah saya tentang Time Slicing dan Suspense, maka bagian kedua termasuk komponen yang membaca data dari cache:


 function MovieDetails({ id }) { //         ? const movie = movieCache.read(id); } 

(Laporan menggunakan API yang sedikit berbeda, tetapi bukan itu intinya.)


Kode ini didasarkan pada fungsi Bereaksi untuk sampel data yang disebut " Suspense ", yang saat ini sedang dalam pengembangan aktif. Hal yang menarik di sini, tentu saja, adalah bahwa data mungkin belum ada di movieCache - dalam hal ini, kita perlu melakukan sesuatu terlebih dahulu, karena kita tidak dapat melanjutkan eksekusi. Secara teknis, dalam hal ini panggilan untuk membaca () melempar Janji (ya, lempar Janji - Anda harus menelan fakta ini). Ini menjeda eksekusi. Bereaksi memotong Janji ini dan ingat bahwa perlu mengulang rendering pohon komponen setelah Janji yang dilempar memenuhi.


Ini bukan efek aljabar dalam dirinya sendiri, meskipun penciptaan trik ini terinspirasi oleh mereka. Trik ini mencapai tujuan yang sama: beberapa kode di bawah ini di tumpukan panggilan sementara lebih rendah daripada sesuatu yang lebih tinggi dalam tumpukan panggilan (dalam hal ini, Bereaksi), sementara semua fungsi antara tidak harus tahu tentang hal itu atau "diracuni" oleh async atau generator. Tentu saja, kita tidak bisa "benar-benar" melanjutkan eksekusi dalam JavaScript, tetapi dari sudut pandang React, menampilkan ulang pohon komponen setelah izin Promise hampir sama. Anda dapat menipu ketika model pemrograman Anda mengasumsikan idempotensi!


Hooks adalah contoh lain yang dapat mengingatkan Anda tentang efek aljabar. Salah satu pertanyaan pertama yang diajukan orang adalah: di mana panggilan useState "tahu" komponen mana yang dimaksud?


 function LikeButton() { //  useState ,    ? const [isLiked, setIsLiked] = useState(false); } 

Saya sudah menjelaskan ini di akhir artikel ini : di objek Bereaksi ada keadaan "pengirim saat ini", yang menunjukkan implementasi yang sedang Anda gunakan (misalnya, seperti dalam react-dom ). Demikian pula, ada properti komponen saat ini yang menunjuk ke struktur data internal LikeButton. Inilah cara useState mengetahui apa yang harus dilakukan.


Sebelum terbiasa, orang sering berpikir bahwa itu terlihat seperti retasan kotor karena alasan yang jelas. Adalah salah untuk mengandalkan keadaan umum yang bisa berubah. (Catatan: menurut Anda bagaimana coba / tangkap diterapkan di mesin JavaScript?)


Namun, secara konseptual Anda dapat mempertimbangkan useState () sebagai efek dari eksekusi State (), yang diproses oleh React ketika komponen Anda dieksekusi. Ini "menjelaskan" mengapa Bereaksi (apa panggilan komponen Anda) dapat menyediakannya dengan keadaan (lebih tinggi di tumpukan panggilan, sehingga dapat memberikan penangan efek). Memang, penerapan keadaan eksplisit adalah salah satu contoh paling umum dalam buku teks tentang efek aljabar yang saya temui.


Sekali lagi, tentu saja, ini bukan cara Bereaksi sebenarnya bekerja, karena kita tidak memiliki efek aljabar dalam JavaScript. Sebagai gantinya, ada bidang tersembunyi di mana kami menyimpan komponen saat ini, serta bidang yang menunjuk ke "dispatcher" saat ini dengan implementasi useState. Sebagai pengoptimalan kinerja, bahkan ada implementasi useState yang terpisah untuk pemasangan dan pembaruan . Tetapi jika Anda sekarang sangat terpelintir oleh kode ini, maka Anda dapat menganggap mereka sebagai penangan efek biasa.


Ringkasnya, kita dapat mengatakan bahwa dalam JavaScript throw dapat berfungsi sebagai perkiraan pertama untuk efek input-output (asalkan kode dapat dieksekusi kembali dengan aman nanti, dan sampai diikat ke CPU), dan bidang variabelnya adalah β€œ dispatcher "dikembalikan dalam percobaan / akhirnya dapat berfungsi sebagai perkiraan kasar untuk penangan efek sinkron.


Anda bisa mendapatkan implementasi efek dengan kualitas yang jauh lebih tinggi menggunakan generator , tetapi ini berarti Anda harus meninggalkan sifat fungsi JavaScript yang "transparan" dan Anda harus melakukan semuanya dengan generator. Dan ini "baik, itu ..."


Di mana mencari tahu lebih lanjut


Secara pribadi, saya terkejut betapa banyak efek aljabar yang diperoleh untuk saya. Saya selalu mencoba yang terbaik untuk memahami konsep abstrak, seperti monad, tetapi efek aljabar hanya mengambil dan "dihidupkan" di kepala. Saya harap artikel ini akan membantu mereka untuk "bergabung" dengan Anda.


Saya tidak tahu apakah mereka akan mulai digunakan secara massal. Saya pikir saya akan kecewa jika mereka tidak mengakar di salah satu bahasa utama pada tahun 2025. Ingatkan saya untuk check-in lima tahun!


Saya yakin Anda bisa melakukan jauh lebih menarik dengan mereka, tetapi sangat sulit untuk merasakan kekuatan mereka sampai Anda mulai menulis kode dan menggunakannya. Jika posting ini membangkitkan rasa ingin tahu Anda, berikut adalah beberapa sumber lainnya tempat Anda dapat membaca lebih detail:



Banyak orang juga menunjukkan bahwa jika Anda menghilangkan aspek pengetikan (seperti yang saya lakukan dalam artikel ini), Anda dapat menemukan penggunaan sebelumnya dari teknik seperti itu dalam sistem kondisi di Common Lisp. , , call/cc .

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


All Articles