Jika Anda mencoba tangan Anda pada pemrograman fungsional, itu berarti bahwa Anda akan segera menemukan konsep fungsi murni. Ketika Anda melanjutkan, Anda akan menemukan bahwa programmer yang lebih suka gaya fungsional tampaknya terobsesi dengan fitur-fitur ini. Mereka mengatakan bahwa fungsi murni memungkinkan Anda untuk berbicara tentang kode. Mereka mengatakan bahwa fungsi murni adalah entitas yang tidak mungkin bekerja begitu tak terduga sehingga akan menyebabkan perang termonuklir. Anda juga dapat belajar dari programmer seperti itu bahwa fungsi murni memberikan transparansi referensial. Dan begitu - hingga tak terbatas.
Omong-omong, programmer fungsional benar. Fungsi murni bagus. Tapi ada satu masalah ...
Penulis bahan, terjemahan yang kami sajikan untuk perhatian Anda, ingin berbicara tentang cara menangani efek samping dalam fungsi murni.
Masalah fungsi murni
Fungsi murni adalah fungsi yang tidak memiliki efek samping (pada kenyataannya, ini bukan definisi lengkap dari fungsi murni, tetapi kami akan kembali ke definisi ini). Namun, jika Anda setidaknya memahami sesuatu dalam pemrograman, maka Anda tahu bahwa hal terpenting di sini adalah efek sampingnya. Mengapa menghitung angka Pi hingga desimal keseratus jika tidak ada yang bisa membaca angka ini? Untuk menampilkan sesuatu di layar atau mencetak pada printer, atau menyajikannya dalam bentuk lain, dapat diakses untuk persepsi, kita perlu memanggil perintah yang sesuai dari program. Dan apa gunanya database jika tidak ada yang bisa dituliskan kepada mereka? Untuk memastikan pengoperasian aplikasi, Anda perlu membaca data dari perangkat input dan meminta informasi dari sumber daya jaringan. Semua ini tidak dapat dilakukan tanpa efek samping. Namun terlepas dari keadaan ini, pemrograman fungsional dibangun di sekitar fungsi murni. Jadi, bagaimana para programmer yang menulis program dengan gaya fungsional dapat mengatasi paradoks ini?
Jika Anda menjawab pertanyaan ini secara singkat, maka programmer fungsional melakukan hal yang sama seperti ahli matematika: mereka curang. Meskipun, terlepas dari tuduhan ini, harus dikatakan bahwa mereka, dari sudut pandang teknis, cukup mengikuti aturan tertentu. Tetapi mereka menemukan celah dalam aturan ini dan memperluasnya ke ukuran yang luar biasa. Mereka melakukan ini dalam dua cara utama:
- Mereka memanfaatkan injeksi ketergantungan. Saya menyebutnya melemparkan masalah ke pagar.
- Mereka menggunakan functors, yang bagi saya merupakan bentuk penundaan yang ekstrem. Perlu dicatat di sini bahwa di Haskell itu disebut "IO functor" atau "IO monad", dalam PureScript istilah "Efek" digunakan, yang, menurut pendapat saya , sedikit lebih baik untuk menggambarkan esensi dari functors.
Injeksi Ketergantungan
Injeksi ketergantungan adalah cara pertama untuk mengatasi efek samping. Dengan menggunakan pendekatan ini, kami mengambil semua yang mencemari kode dan memasukkannya ke dalam parameter fungsi. Maka kita dapat menganggap semua ini sebagai sesuatu yang merupakan bagian dari tanggung jawab beberapa fungsi lainnya. Saya akan menjelaskan ini dengan contoh berikut:
// logSomething :: String -> String function logSomething(something) { const dt = (new Date())toISOString(); console.log(`${dt}: ${something}`); return something; }
Di sini saya ingin membuat catatan untuk mereka yang terbiasa dengan tanda tangan jenis. Jika kita benar-benar mematuhi aturan, maka kita harus memperhitungkan efek sampingnya di sini. Tapi kita akan membahasnya nanti.
Fungsi
logSomething()
memiliki dua masalah yang mencegahnya dari dinyatakan bersih: ia membuat objek
Date
dan mengeluarkan sesuatu ke konsol. Artinya, fungsi kami tidak hanya melakukan operasi input-output, tetapi juga menghasilkan, ketika dipanggil pada waktu yang berbeda, hasil yang berbeda.
Bagaimana membuat fungsi ini bersih? Dengan menggunakan teknik injeksi ketergantungan, kita dapat mengambil semua yang mencemari fungsi dan menjadikannya parameter fungsi. Akibatnya, alih-alih menerima satu parameter, fungsi kami akan menerima tiga parameter:
// logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) { const dt = d.toIsoString(); return cnsl.log(`${dt}: ${something}`); }
Sekarang, untuk memanggil fungsi, kita perlu mentransfer semuanya yang mencemari itu sebelumnya:
const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something);
Di sini Anda mungkin berpikir bahwa semua ini omong kosong, bahwa kami hanya memindahkan masalah satu tingkat ke atas, dan ini tidak menambah kemurnian pada kode kami. Dan Anda tahu, ini adalah pikiran yang benar. Ini adalah celah dalam bentuknya yang paling murni.
Ini seperti ketidaktahuan pura-pura: "Saya tidak tahu bahwa memanggil metode
log
dari objek
cnsl
mengarah pada eksekusi pernyataan I / O. Seseorang baru saja menyerahkannya kepada saya, tetapi saya tidak tahu dari mana semua itu berasal. " Sikap ini salah.
Dan, pada kenyataannya, apa yang terjadi tidak sebodoh yang terlihat pada pandangan pertama.
logSomething()
fitur fungsi
logSomething()
. Jika Anda ingin melakukan sesuatu yang najis, maka Anda harus melakukannya sendiri. Katakanlah Anda dapat meneruskan berbagai parameter ke fungsi ini:
const d = {toISOString: () => '1865-11-26T16:00:00.000Z'}; const cnsl = { log: () => { // }, }; logSomething(d, cnsl, "Off with their heads!"); // "Off with their heads!"
Sekarang fungsi kita tidak melakukan apa-apa (hanya mengembalikan parameter
something
). Tapi dia benar-benar murni. Jika Anda memanggilnya dengan parameter yang sama beberapa kali, itu akan mengembalikan hal yang sama setiap kali. Dan itulah intinya. Untuk membuat fungsi ini tidak bersih, kita perlu melakukan tindakan tertentu dengan sengaja. Atau, dengan kata lain, segala sesuatu yang bergantung pada fungsi ada dalam tanda tangannya. Itu tidak mengakses objek global seperti
console
atau
Date
. Ini meresmikan segalanya.
Selain itu, penting untuk dicatat bahwa kita dapat mentransfer fungsi lain ke fungsi kita, yang sebelumnya tidak bersih. Lihatlah contoh lain. Bayangkan bahwa dalam beberapa bentuk ada nama pengguna dan kita perlu mendapatkan nilai dari bidang yang sesuai dari formulir ini:
// getUserNameFromDOM :: () -> String function getUserNameFromDOM() { return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; // "mhatter"
Dalam hal ini, kami mencoba memuat beberapa informasi dari DOM. Fungsi murni tidak melakukan ini, karena
document
adalah objek global yang dapat berubah kapan saja. Salah satu cara untuk membuat fungsi tersebut bersih adalah dengan mengirimkannya objek
document
global sebagai parameter. Namun, Anda masih bisa meneruskan fungsi
querySelector()
ke
querySelector()
. Ini terlihat seperti ini:
// getUserNameFromDOM :: (String -> Element) -> String function getUserNameFromDOM($) { return $('#username').value; } // qs :: String -> Element const qs = document.querySelector.bind(document); const username = getUserNameFromDOM(qs); username; // "mhatter"
Di sini, sekali lagi, Anda mungkin berpikir bahwa ini bodoh. Bagaimanapun, di sini kita cukup menghapus dari fungsi
getUsernameFromDOM()
yang tidak memungkinkan kita untuk menyebutnya bersih. Namun, kami tidak menyingkirkan ini, hanya mentransfer panggilan ke DOM ke fungsi lain,
qs()
. Mungkin terlihat bahwa satu-satunya hasil nyata dari langkah ini adalah bahwa kode baru lebih panjang dari yang lama. Alih-alih satu fungsi najis, kita sekarang memiliki dua fungsi, yang salah satunya masih najis.
Tunggu sebentar. Bayangkan kita perlu menulis tes untuk fungsi
getUserNameFromDOM()
. Sekarang, membandingkan dua opsi untuk fungsi ini, pikirkan yang mana yang akan lebih mudah untuk dikerjakan? Agar versi fungsi yang kotor berfungsi sama sekali, kita memerlukan objek dokumen global. Selain itu, dokumen ini harus memiliki elemen dengan pengenal
username
. Jika Anda perlu menguji fungsi serupa di luar browser, maka Anda perlu menggunakan sesuatu seperti JSDOM atau browser tanpa antarmuka pengguna. Harap dicatat bahwa semua ini diperlukan hanya untuk menguji fungsi kecil dengan panjang beberapa baris. Dan untuk menguji versi kedua yang bersih dari fungsi ini, cukup melakukan hal berikut:
const qsStub = () => ({value: 'mhatter'}); const username = getUserNameFromDOM(qsStub); assert.strictEqual('mhatter', username, `Expected username to be ${username}`);
Ini, tentu saja, tidak berarti bahwa untuk menguji fungsi-fungsi tersebut, tes integrasi dilakukan di browser nyata (atau, setidaknya, menggunakan sesuatu seperti JSDOM) tidak diperlukan. Tetapi contoh ini menunjukkan hal yang sangat penting, yaitu bahwa sekarang fungsi
getUserNameFromDOM()
sepenuhnya dapat diprediksi. Jika kita meneruskan
qsStub()
ke sana, ia akan selalu mengembalikan
mhatter
. "Ketidakpastian" kami telah pindah ke fungsi kecil
qs()
.
Jika perlu, kita dapat mengambil mekanisme yang tidak terduga ke level yang lebih jauh dari fungsi utama. Sebagai hasilnya, kita dapat memindahkannya, secara relatif, ke βarea perbatasanβ kode. Ini akan menghasilkan kita memiliki cangkang tipis kode najis yang mengelilingi kernel yang telah teruji dan dapat diprediksi. Prediktabilitas kode ternyata sangat berharga ketika ukuran proyek yang dibuat oleh programmer bertambah.
β Kerugian dari mekanisme injeksi ketergantungan
Menggunakan injeksi ketergantungan, Anda dapat menulis aplikasi yang besar dan kompleks. Saya tahu ini, karena saya sendiri yang menulis
aplikasi semacam itu . Dengan pendekatan ini, pengujian disederhanakan, dan dependensi fungsi menjadi jelas terlihat. Tetapi injeksi ketergantungan bukan tanpa cacat. Yang utama adalah bahwa ketika digunakan, tanda tangan fungsi yang sangat panjang dapat diperoleh:
function app(doc, con, ftch, store, config, ga, d, random) { // } app(document, console, fetch, store, config, ga, (new Date()), Math.random);
Sebenarnya, ini tidak terlalu buruk. Kerugian dari konstruksi tersebut dimanifestasikan jika beberapa parameter perlu diteruskan ke fungsi-fungsi tertentu yang sangat tertanam dalam fungsi-fungsi lain. Sepertinya kebutuhan untuk melewati parameter melalui banyak level panggilan fungsi. Ketika jumlah level tersebut meningkat, itu mulai mengganggu. Misalnya, mungkin perlu untuk mentransfer objek yang mewakili tanggal melalui 5 fungsi antara, sementara tidak ada fungsi antara yang menggunakan objek ini. Meskipun, tentu saja, tidak dapat dikatakan bahwa situasi seperti itu adalah semacam bencana universal. Selain itu, ini memungkinkan untuk melihat dengan jelas ketergantungan fungsi. Namun, meskipun demikian, ini masih tidak begitu menyenangkan. Karena itu, kami mempertimbangkan mekanisme berikut.
Functions Fungsi malas
Mari kita lihat celah kedua yang digunakan oleh penganut pemrograman fungsional. Ini terdiri dalam ide berikut: efek samping bukanlah efek samping sampai benar-benar terjadi. Saya tahu itu terdengar misterius. Untuk mengetahui hal ini, pertimbangkan contoh berikut:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
Contohnya mungkin bodoh, saya tahu itu. Jika kita membutuhkan angka 0, maka untuk membuatnya muncul, cukup masukkan di tempat yang tepat dalam kode. Dan saya juga tahu bahwa Anda tidak akan menulis kode JavaScript untuk mengendalikan senjata nuklir. Tetapi kita membutuhkan kode ini untuk menggambarkan teknologi yang dimaksud.
Jadi di sini adalah contoh dari fungsi yang tidak murni. Ini output data ke konsol dan juga merupakan penyebab perang nuklir. Namun, bayangkan bahwa kita membutuhkan nol yang mengembalikan fungsi ini. Bayangkan sebuah skenario di mana kita perlu menghitung sesuatu setelah meluncurkan roket. Katakanlah kita mungkin perlu memulai penghitung waktu mundur atau sesuatu seperti itu. Dalam hal ini, sangat wajar untuk berpikir terlebih dahulu tentang melakukan perhitungan. Dan kita harus memastikan bahwa roket itu diluncurkan tepat saat dibutuhkan. Kita tidak perlu melakukan perhitungan sedemikian rupa sehingga mereka dapat secara tidak sengaja mengarah pada peluncuran roket ini. Jadi mari kita pikirkan apa yang terjadi jika kita membungkus fungsi
fZero()
di fungsi lain yang mengembalikannya. Katakanlah itu akan menjadi sesuatu seperti pembungkus keamanan:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; } // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() { return fZero; }
Anda dapat memanggil fungsi
returnZeroFunc()
sebanyak yang Anda suka. Dalam hal ini, sampai implementasi apa yang dikembalikan dilakukan, kami (secara teoritis) aman. Dalam kasus kami, ini berarti mengeksekusi kode berikut tidak akan mengarah pada perang nuklir:
const zeroFunc1 = returnZeroFunc(); const zeroFunc2 = returnZeroFunc(); const zeroFunc3 = returnZeroFunc();
Sekarang sedikit lebih ketat dari sebelumnya, mari kita mendekati definisi istilah "fungsi murni". Ini akan memungkinkan kita untuk memeriksa fungsi
returnZeroFunc()
lebih terinci. Jadi, fungsinya bersih dalam kondisi berikut:
- Tidak ada efek samping yang diamati.
- Tautan transparansi. Artinya, memanggil fungsi seperti itu dengan nilai input yang sama selalu mengarah ke hasil yang sama.
returnZeroFunc()
menganalisis fungsi
returnZeroFunc()
.
Apakah dia punya efek samping? Kami baru tahu bahwa memanggil
returnZeroFunc()
tidak meluncurkan rudal. Jika Anda tidak memanggil apa fungsi ini kembali, tidak ada yang akan terjadi. Karena itu, kita dapat menyimpulkan bahwa fungsi ini tidak memiliki efek samping.
Apakah fitur ini transparan secara referensi? Yaitu, apakah selalu kembali sama ketika mengirimkan data input yang sama? Kami akan memverifikasi ini, mengambil keuntungan dari fakta bahwa dalam fragmen kode di atas kami memanggil fungsi ini beberapa kali:
zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true
Semuanya terlihat bagus, tetapi fungsi
returnZeroFunc()
belum sepenuhnya bersih. Dia merujuk ke variabel yang berada di luar ruang lingkupnya sendiri. Untuk mengatasi masalah ini, kami menulis ulang fungsinya:
// returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() { function fZero() { console.log('Launching nuclear missiles'); // return 0; } return fZero; }
Sekarang fungsinya bisa dianggap bersih. Namun, dalam situasi ini, aturan JavaScript bermain melawan kami. Yaitu, kita tidak bisa lagi menggunakan operator
===
untuk memeriksa transparansi referensial dari suatu fungsi. Ini karena fakta bahwa
returnZeroFunc()
akan selalu mengembalikan referensi baru ke fungsi. Benar, transparansi tautan dapat diverifikasi dengan memeriksa sendiri kodenya. Analisis semacam itu akan menunjukkan bahwa dengan setiap fungsi panggilan itu mengembalikan tautan ke fungsi yang sama.
Di depan kita ada celah kecil yang rapi. Tetapi dapatkah itu digunakan dalam proyek nyata? Jawaban atas pertanyaan ini positif. Namun, sebelum berbicara tentang bagaimana menggunakan ini dalam praktik, kami akan mengembangkan ide kami sedikit. Yaitu,
fZero()
kembali ke fungsi berbahaya
fZero()
:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
Kami akan mencoba menggunakan nol yang dikembalikan oleh fungsi ini, tetapi kami akan melakukannya sehingga (sejauh ini) perang nuklir tidak dimulai. Untuk melakukan ini, buat fungsi yang mengambil nol dikembalikan oleh fungsi
fZero()
dan menambahkannya:
// fIncrement :: (() -> Number) -> Number function fIncrement(f) { return f() + 1; } fIncrement(fZero); // // 1
Itu nasib buruk ... Kami tidak sengaja memulai perang nuklir. Mari kita coba lagi, tapi kali ini kami tidak akan mengembalikan nomor. Sebagai gantinya, kami mengembalikan fungsi yang suatu hari nanti mengembalikan nomor:
// fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) { return () => f() + 1; } fIncrement(zero); // [Function]
Sekarang Anda bisa bernapas dengan mudah. Bencana itu dihindari. Kami melanjutkan studi. Berkat dua fungsi ini, kami dapat membuat sejumlah "angka yang mungkin":
const fOne = fIncrement(zero); const fTwo = fIncrement(one); const fThree = fIncrement(two);
Selain itu, kita dapat membuat banyak fungsi yang namanya akan dimulai dengan
f
(sebut saja
f*()
), dirancang untuk bekerja dengan "angka yang mungkin":
// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number) function fMultiply(a, b) { return () => a() * b(); } // fPow :: (() -> Number) -> (() -> Number) -> (() -> Number) function fPow(a, b) { return () => Math.pow(a(), b()); } // fSqrt :: (() -> Number) -> (() -> Number) function fSqrt(x) { return () => Math.sqrt(x()); } const fFour = fPow(fTwo, fTwo); const fEight = fMultiply(fFour, fTwo); const fTwentySeven = fPow(fThree, fThree); const fNine = fSqrt(fTwentySeven); // , . !
Lihat apa yang kami lakukan di sini? Dengan "kemungkinan angka" Anda dapat melakukan hal yang sama dengan angka biasa. Matematikawan menyebut ini
isomorfisme . Angka biasa selalu dapat diubah menjadi "kemungkinan angka" dengan menempatkannya dalam suatu fungsi. Anda bisa mendapatkan "kemungkinan nomor" dengan memanggil fungsi. Dengan kata lain, kami memiliki pemetaan antara angka biasa dan "angka yang mungkin." Ini, pada kenyataannya, jauh lebih menarik daripada yang terlihat. Kami akan segera kembali ke ide ini.
Teknik di atas menggunakan fungsi wrapper adalah strategi yang valid. Kita dapat bersembunyi di balik fungsi sebanyak yang diperlukan. Dan, karena kita belum menyebut fungsi-fungsi ini, semuanya, secara teoritis, murni. Dan tidak ada yang memulai perang. Dalam kode reguler (tidak terkait roket), kita benar-benar membutuhkan efek samping pada akhirnya. Membungkus semua yang kita butuhkan menjadi suatu fungsi memungkinkan kita untuk mengontrol efek-efek ini dengan tepat. Kami memilih waktu ketika efek ini muncul.
Perlu dicatat bahwa sangat tidak nyaman untuk menggunakan konstruksi yang seragam dengan banyak tanda kurung di mana-mana untuk menyatakan fungsi. Dan menciptakan versi baru dari setiap fungsi juga bukan kegiatan yang menyenangkan. JavaScript memiliki beberapa fungsi
Math.sqrt()
seperti
Math.sqrt()
. Alangkah baiknya jika ada cara untuk menggunakan fungsi-fungsi biasa ini dengan "nilai yang tertunda" kami. Sebenarnya, kita akan membicarakan ini sekarang.
Efek Functor
Di sini kita akan berbicara tentang functors yang diwakili oleh objek yang berisi "fungsi yang ditangguhkan". Untuk mewakili functor, kita akan menggunakan objek
Effect
. Kami akan menempatkan fungsi kami
fZero()
di objek seperti itu. Tapi, sebelum melakukan ini, kita akan membuat fungsi ini sedikit lebih aman:
// zero :: () -> Number function fZero() { console.log('Starting with nothing'); // , , . // . return 0; }
Sekarang kita menggambarkan fungsi konstruktor untuk membuat objek dengan tipe
Effect
:
// Effect :: Function -> Effect function Effect(f) { return {}; }
Tidak ada yang menarik di sini sehingga kami akan mengerjakan fitur ini. Jadi, kami ingin menggunakan fungsi
fZero()
biasa dengan objek
Effect
. Untuk memberikan skenario seperti itu, kami akan menulis metode yang menerima fungsi reguler dan suatu hari akan menerapkannya pada "nilai yang tertunda" kami. Dan kami akan melakukan ini tanpa memanggil fungsi
Effect
. Kami menyebutnya
map()
fungsi
map()
. Ini memiliki nama seperti itu karena ia menciptakan pemetaan antara fungsi biasa dan fungsi
Effect
. Ini mungkin terlihat seperti ini:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); } } }
Sekarang, jika Anda memonitor dengan cermat apa yang terjadi, Anda mungkin memiliki pertanyaan tentang fungsi
map()
. Kelihatannya mirip dengan lagu itu. Kami akan kembali ke masalah ini nanti, tetapi untuk saat ini kami akan menguji apa yang kami miliki saat ini dalam tindakan:
const zero = Effect(fZero); const increment = x => x + 1;
Jadi ... Sekarang kita tidak memiliki kesempatan untuk mengamati apa yang terjadi di sini. Karena itu, mari kita modifikasi
Effect
agar, untuk berbicara, dapatkan kesempatan untuk "menarik pelatuk":
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } } } const zero = Effect(fZero); const increment = x => x + 1; // . const one = zero.map(increment); one.runEffects(); // // 1
Jika perlu, kita dapat terus memanggil fungsi
map()
:
const double = x => x * 2; const cube = x => Math.pow(x, 3); const eight = Effect(fZero) .map(increment) .map(double) .map(cube); eight.runEffects();
Di sini, apa yang terjadi sudah mulai menjadi lebih menarik. Kami menyebutnya "functor." Semua ini berarti bahwa objek
Effect
memiliki fungsi
map()
dan mematuhi beberapa
aturan . Namun, ini bukan aturan yang melarang apa pun. Aturan-aturan ini adalah tentang apa yang dapat Anda lakukan. Mereka lebih seperti hak istimewa. Karena objek
Effect
adalah functor, ia mematuhi aturan ini. Secara khusus, ini adalah apa yang disebut "aturan komposisi".
Ini terlihat seperti ini:
Jika ada objek
Effect
bernama
e
, dan dua fungsi,
f
dan
g
, maka
e.map(g).map(f)
setara dengan
e.map(x => f(g(x)))
.
Dengan kata lain, dua metode
map()
consecutive
map()
sama dengan menyusun dua fungsi. Ini berarti bahwa objek dengan tipe
Effect
dapat melakukan tindakan yang mirip dengan yang berikut (ingat salah satu contoh di atas):
const incDoubleCube = x => cube(double(increment(x)))
Ketika kita melakukan apa yang diperlihatkan di sini, kita dijamin mendapatkan hasil yang sama seperti kita menggunakan versi kode ini dengan triple call to
map()
. Kita dapat menggunakan ini saat refactoring kode, dan kita dapat yakin bahwa kode akan bekerja dengan benar. Dalam beberapa kasus, mengubah satu pendekatan ke yang lain bahkan dapat meningkatkan kinerja.
Sekarang saya mengusulkan untuk berhenti bereksperimen dengan angka dan berbicara tentang apa yang lebih mirip kode yang digunakan dalam proyek nyata.
EtMetode dari ()
Konstruktor objek
Effect
menerima, sebagai argumen, fungsi. Ini nyaman, karena sebagian besar efek samping yang ingin kami tunda adalah fungsi. Misalnya, ini adalah
Math.random()
dan
console.log()
. Namun, terkadang Anda perlu memberi nilai pada objek
Effect
yang bukan fungsi. , ,
window
. , . , ( -, , , Haskell
pure
):
// of :: a -> Effect a Effect.of = function of(val) { return Effect(() => val); }
, , , -. , , . HTML- . , . . Sebagai contoh:
window.myAppConf = { selectors: { 'user-bio': '.userbio', 'article-list': '#articles', 'user-name': '.userfullname', }, templates: { 'greet': 'Pleased to meet you, {name}', 'notify': 'You have {n} alerts', } };
,
Effect.of()
,
Effect
:
const win = Effect.of(window); userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']);
β Effect
. ,
Effect
. ,
getElementLocator()
,
Effect
, . DOM,
document.querySelector()
β , . :
// $ :: String -> Effect DOMElement function $(selector) { return Effect.of(document.querySelector(s)); }
, ,
map()
:
const userBio = userBioLocator.map($);
, , .
div
,
map()
, , . ,
innerHTML
, :
const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
, .
userBio
, . , , , . , ,
Effect('user-bio')
. , , , :
Effect(() => '.userbio');
β . :
Effect(() => window.myAppConf.selectors['user-bio']);
,
map()
, ( ). , ,
$
, :
Effect(() => $(window.myAppConf.selectors['user-bio']))
, :
Effect( () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio']))) );
Effect.of
, :
Effect( () => Effect( () => document.querySelector(window.myAppConf.selectors['user-bio']) ) );
, , , .
Effect
.
β join()
? ,
Effect
. , , .
Effect
.runEffect()
. . , - , , , , . , .
join()
.
Effect
,
runEffect()
, . , .
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } } }
, :
const userBioHTML = Effect.of(window) .map(x => x.myAppConf.selectors['user-bio']) .map($) .join() .map(x => x.innerHTML);
β chain()
,
.map()
,
.join()
, . , , . , ,
Effect
. ,
.map()
.join()
. , ,
Effect
:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } chain(g) { return Effect(f).map(g).join(); } } }
chain()
- , ,
Effect
( ,
). HTML- :
const userBioHTML = Effect.of(window) .map(x => x.myAppConf.selectors['user-bio']) .chain($) .map(x => x.innerHTML);
-. . ,
flatMap
. , , β , ,
join()
. Haskell, ,
bind
. , - , ,
chain
,
flatMap
bind
β .
β Effect
Effect
, . . , DOM, , ? , , , . , . β
.
// tpl :: String -> Object -> String const tpl = curry(function tpl(pattern, data) { return Object.keys(data).reduce( (str, key) => str.replace(new RegExp(`{${key}}`, data[key]), pattern ); });
. :
const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str});
, . . (
name
pattern
)
Effect
.
tpl()
, ,
Effect
.
,
map()
Effect
tpl()
:
pattern.map(tpl);
, .
map()
:
map :: Effect a ~> (a -> b) -> Effect b
:
tpl :: String -> Object -> String
,
map()
pattern
, ( ,
tpl()
)
Effect
.
Effect (Object -> String)
pattern
Effect
. .
Effect
, .
ap()
:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } chain(g) { return Effect(f).map(g).join(); } ap(eff) { // - ap, , eff ( ). // map , eff ( 'g') // g, f() return eff.map(g => g(f())); } } }
.ap()
:
const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str})); const pattern = win.map(w => w.myAppConfig.templates('greeting')); const greeting = name.ap(pattern.map(tpl));
, β¦ , ,
.ap()
. , ,
map()
,
ap()
. , , .
. , . , , ,
Effect
,
ap()
. , :
// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c) const liftA2 = curry(function liftA2(f, x, y) { return y.ap(x.map(f)); // : // return x.map(f).chain(g => y.map(g)); });
liftA2()
, , .
liftA3()
:
// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d) const liftA3 = curry(function liftA3(f, a, b, c) { return c.ap(b.ap(a.map(f))); });
,
liftA2()
liftA3()
Effect
. , ,
ap()
.
liftA2()
:
const win = Effect.of(window); const user = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str}); const pattern = win.map(w => w.myAppConfig.templates['greeting']); const greeting = liftA2(tpl)(pattern, user);
?
, , , . ? ,
Effect
ap()
. , ? ?
: Β« , , Β».
:
β
β . , , , .
const pattern = window.myAppConfig.templates['greeting'];
, , , :
const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting'));
β , , , , . . β , , . , , , , , , . , . β . , , , . , .
. .
β Effect
, , . -
Facebook
Gmail
. ? .
, . . CSV- . . , , , . , . , . , , , .
, . ,
map()
reduce()
, . . , . , , , . 4 (, , 8, 16, ). , , . , . , - .
, , . , . Tidak menyerupai apa pun? , , , . . , .
TensorFlow , .
TensorFlow, , . «». , , :
node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2)
Python, JavaScript. ,
Effect
,
add()
, (
sess.run()
).
print("node3: ", node3) print("sess.run(node3): ", sess.run(node3)) # node3: Tensor("Add_2:0", shape=(), dtype=float32) # sess.run(node3): 7.0
, (7.0) ,
sess.run()
. , . , , , .
Ringkasan
, . , .
Effect
.
, , , , , . , , .
Effect
, , , . , .
β . , . , , . . . , .
Pembaca yang budiman! ?