Mari kita bicara sedikit tentang bagaimana kita menangani kesalahan. Dalam JavaScript, kami memiliki fungsi bahasa bawaan untuk bekerja dengan pengecualian. Kami menyertakan kode bermasalah di
try...catch
konstruk. Ini memungkinkan Anda untuk menentukan jalur eksekusi normal di bagian
try
, dan kemudian berurusan dengan semua pengecualian di bagian
catch
. Bukan pilihan yang buruk. Ini memungkinkan Anda untuk fokus pada tugas saat ini tanpa memikirkan setiap kesalahan yang mungkin terjadi. Jelas lebih baik daripada menyumbat kode Anda dengan ifs tanpa akhir.
Tanpa
try...catch
sulit untuk memeriksa hasil dari setiap pemanggilan fungsi untuk nilai yang tidak terduga. Ini adalah desain yang bermanfaat. Tapi dia punya masalah tertentu. Dan ini bukan satu-satunya cara untuk menangani kesalahan. Pada artikel ini, kita akan melihat menggunakan
monad Either sebagai alternatif untuk
try...catch
.
Sebelum melanjutkan, saya perhatikan beberapa poin. Artikel ini mengasumsikan bahwa Anda sudah tahu tentang komposisi fungsi dan kari. Dan sebuah peringatan. Jika Anda belum pernah bertemu monad sebelumnya, mereka dapat terlihat sangat ... aneh. Bekerja dengan alat semacam itu membutuhkan perubahan dalam berpikir. Pada awalnya itu bisa sulit.
Jangan khawatir jika Anda langsung bingung. Setiap orang memilikinya. Di akhir artikel, saya mencantumkan beberapa tautan yang mungkin membantu. Jangan menyerah. Hal-hal ini dimabukkan begitu mereka menembus otak.
Contoh masalah
Sebelum membahas masalah pengecualian, mari kita bicara tentang mengapa mereka ada dan mengapa
try...catch
blok muncul. Untuk melakukan ini, mari kita lihat masalah yang saya coba buat setidaknya sebagian realistis. Bayangkan kita sedang menulis fungsi untuk menampilkan daftar notifikasi. Kami telah berhasil (entah bagaimana) mengembalikan data dari server. Tetapi untuk beberapa alasan, para insinyur backend memutuskan untuk mengirimkannya dalam format CSV, bukan JSON. Data mentah mungkin terlihat seperti ini:
cap waktu, konten, dilihat, href
2018-10-27T05: 33: 34 + 00: 00, @ madhatter mengundang Anda untuk minum teh, belum membaca, https: //example.com/invite/tea/3801
2018-10-26T13: 47: 12 + 00: 00, @ queenofhearts menyebut Anda dalam diskusi 'Croquet Tournament', dilihat, https: //example.com/discussions/croquet/1168
2018-10-25T03: 50: 08 + 00: 00, @ cheshirecat mengirimi Anda senyuman, belum dibaca, https: //example.com/interactions/grin/88
Kami ingin menampilkannya dalam HTML. Mungkin terlihat seperti ini:
<ul class="MessageList"> <li class="Message Message--viewed"> <a href="https://example.com/invite/tea/3801" class="Message-link">@madhatter invited you to tea</a> <time datetime="2018-10-27T05:33:34+00:00">27 October 2018</time> <li> <li class="Message Message--viewed"> <a href="https://example.com/discussions/croquet/1168" class="Message-link">@queenofhearts mentioned you in 'Croquet Tournament' discussion</a> <time datetime="2018-10-26T13:47:12+00:00">26 October 2018</time> </li> <li class="Message Message--viewed"> <a href="https://example.com/interactions/grin/88" class="Message-link">@cheshirecat sent you a grin</a> <time datetime="2018-10-25T03:50:08+00:00">25 October 2018</time> </li> </ul>
Untuk menyederhanakan tugas, fokus saja pada pemrosesan setiap baris data CSV untuk saat ini. Mari kita mulai dengan beberapa fungsi sederhana untuk memproses string. Yang pertama membagi string teks menjadi bidang:
function splitFields(row) { return row.split('","'); }
Fungsi ini disederhanakan di sini karena merupakan bahan pendidikan. Kami menangani penanganan kesalahan, bukan analisis CSV. Jika salah satu pesan berisi koma, semua ini akan sangat salah. Harap tidak pernah menggunakan kode ini untuk menganalisis data CSV nyata. Jika Anda pernah harus menganalisis data CSV, gunakan
parsing pustaka CSV yang teruji dengan
baik .
Setelah memisahkan data, kami ingin membuat objek. Dan agar setiap nama properti cocok dengan header CSV. Misalkan kita sudah entah bagaimana menganalisis bilah judul (lebih lanjut tentang itu nanti). Kami telah sampai pada titik di mana ada sesuatu yang salah. Kami mendapat kesalahan untuk diproses. Kami melempar kesalahan jika panjang string tidak cocok dengan bilah judul. (
_.zipObject
adalah
fungsi lodash ).
function zipRow(headerFields, fieldData) { if (headerFields.length !== fieldData.length) { throw new Error("Row has an unexpected number of fields"); } return _.zipObject(headerFields, fieldData); }
Setelah itu, tambahkan tanggal yang bisa dibaca manusia ke objek untuk menampilkannya di templat kami. Ternyata sedikit bertele-tele, karena JavaScript tidak memiliki dukungan bawaan yang sempurna untuk pemformatan tanggal. Dan lagi, kita menghadapi masalah potensial. Jika tanggal yang tidak valid ditemukan, fungsi kami melempar kesalahan.
function addDateStr(messageObj) { const errMsg = 'Unable to parse date stamp in message object'; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const d = new Date(messageObj.datestamp); if (isNaN(d)) { throw new Error(errMsg); } const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`; return {datestr, ...messageObj}; }
Terakhir, ambil objek dan lewati
fungsi templat untuk mendapatkan string HTML.
const rowToMessage = _.template(`<li class="Message Message--<%= viewed %>"> <a href="<%= href %>" class="Message-link"><%= content %></a> <time datetime="<%= datestamp %>"><%= datestr %></time> <li>`);
Akan lebih baik untuk mencetak kesalahan jika bertemu:
const showError = _.template(`<li class="Error"><%= message %></li>`);
Ketika semuanya sudah ada, Anda dapat mengumpulkan fungsi untuk memproses setiap baris.
function processRow(headerFieldNames, row) { try { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); rowObjWithDate = addDateStr(rowObj); return rowToMessage(rowObj); } catch(e) { return showError(e); } }
Jadi fungsinya sudah siap. Mari kita lihat lebih dekat bagaimana menangani pengecualian.
Pengecualian: bagian yang bagus
Jadi apa gunanya
try...catch
? Perlu dicatat bahwa dalam contoh di atas, salah satu langkah di blok
try
dapat menyebabkan kesalahan. Di
zipRow()
dan
addDateStr()
kami sengaja melakukan kesalahan. Dan jika masalah muncul, tangkap saja kesalahannya dan tampilkan pesan apa pun di halaman. Tanpa mekanisme ini, kode menjadi sangat jelek. Begini tampilannya. Asumsikan bahwa fungsi tidak membuang kesalahan, tetapi mengembalikan
null
.
function processRowWithoutExceptions(headerFieldNames, row) { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); if (rowObj === null) { return showError(new Error('Encountered a row with an unexpected number of items')); } rowObjWithDate = addDateStr(rowObj); if (rowObjWithDate === null) { return showError(new Error('Unable to parse date in row object')); } return rowToMessage(rowObj); }
Seperti yang Anda lihat, sejumlah besar template
if
. Kode lebih verbose. Dan sulit untuk mengikuti logika dasar. Selain itu,
null
tidak memberi tahu kami banyak. Kami tidak benar-benar tahu mengapa panggilan fungsi sebelumnya gagal. Kita harus menebak. Kami membuat pesan kesalahan dan memanggil
showError()
. Kode seperti itu lebih kotor dan lebih membingungkan.
Lihat lagi pada versi penanganan pengecualian. Ini dengan jelas memisahkan jalur program yang sukses dan kode penanganan pengecualian. Cabang
try
adalah cara yang baik, dan cabang
catch
adalah kesalahan. Semua penanganan pengecualian terjadi di satu tempat. Dan fungsi individual dapat melaporkan mengapa mereka gagal. Secara keseluruhan, ini tampaknya cukup manis. Saya pikir mayoritas menganggap contoh pertama cukup cocok. Mengapa pendekatannya berbeda?
Masalah yang menangani pengecualian coba ... tangkap
Pendekatan ini memungkinkan Anda untuk mengabaikan kesalahan yang mengganggu ini. Sayangnya,
try...catch
melakukan tugasnya dengan sangat baik. Anda cukup melempar pengecualian dan melanjutkan. Kita bisa menangkapnya nanti. Dan semua orang berniat untuk selalu menempatkan blok seperti itu, sungguh. Tapi itu tidak selalu jelas di mana kesalahan lebih jauh. Dan bloknya terlalu mudah untuk dilupakan. Dan sebelum Anda menyadarinya, aplikasi Anda macet.
Selain itu, pengecualian mencemari kode. Kami tidak akan membahas kemurnian fungsional secara rinci di sini. Tapi mari kita lihat satu aspek kecil kemurnian fungsional: transparansi referensial. Fungsi tautan-transparan selalu mengembalikan hasil yang sama untuk input tertentu. Tetapi untuk fungsi dengan pengecualian, kita tidak bisa mengatakan itu. Mereka dapat melemparkan pengecualian kapan saja alih-alih mengembalikan nilai. Ini menyulitkan logika. Tetapi bagaimana jika Anda menemukan opsi win-win - cara bersih untuk menangani kesalahan?
Kami datang dengan alternatif
Fungsi murni selalu mengembalikan nilai (bahkan jika nilai itu hilang). Oleh karena itu, kode penanganan kesalahan kami harus mengasumsikan bahwa kami selalu mengembalikan nilai. Jadi, sebagai upaya pertama, apa yang harus saya lakukan jika, saat gagal, kami mengembalikan objek Galat? Artinya, di mana pun kita membuat kesalahan, kita mengembalikan objek seperti itu. Mungkin terlihat seperti ini:
function processRowReturningErrors(headerFieldNames, row) { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); if (rowObj instanceof Error) { return showError(rowObj); } rowObjWithDate = addDateStr(rowObj); if (rowObjWithDate instanceof Error) { return showError(rowObjWithDate); } return rowToMessage(rowObj); }
Ini bukan upgrade khusus tanpa kecuali. Tapi itu lebih baik. Kami telah mengalihkan tanggung jawab atas pesan kesalahan kembali ke fungsi masing-masing. Tapi kita masih memiliki semua seandainya ini. Akan menyenangkan untuk mengenkapsulasi templat tersebut. Dengan kata lain, jika kita tahu bahwa kita memiliki bug, jangan khawatir tentang sisa kode.
Polimorfisme
Bagaimana cara melakukannya? Ini masalah yang sulit. Tapi itu bisa diselesaikan dengan bantuan keajaiban
polimorfisme . Jika Anda belum pernah mengalami polimorfisme sebelumnya, jangan khawatir. Pada dasarnya, ini adalah "menyediakan antarmuka tunggal untuk entitas dari berbagai jenis" (Straustrup, B. "C ++ Glosarium Björn Straustrup"). Dalam JavaScript, ini berarti bahwa kami membuat objek dengan metode dan tanda tangan yang bernama sama. Namun perilaku berbeda. Contoh klasiknya adalah pendataan aplikasi. Kami dapat mengirim majalah kami ke berbagai tempat tergantung pada lingkungan tempat kami berada. Bagaimana jika kita membuat dua objek logger, misalnya?
const consoleLogger = { log: function log(msg) { console.log('This is the console logger, logging:', msg); } }; const ajaxLogger = { log: function log(msg) { return fetch('https://example.com/logger', {method: 'POST', body: msg}); } };
Kedua objek menentukan fungsi log yang mengharapkan parameter string tunggal. Tetapi mereka berperilaku berbeda. Keindahannya adalah kita dapat menulis kode yang memanggil
.log()
, apa pun objek yang digunakannya. Itu bisa berupa
consoleLogger
atau
ajaxLogger
. Semuanya berhasil. Misalnya, kode di bawah ini akan berfungsi sama baiknya dengan objek apa pun:
function log(logger, message) { logger.log(message); }
Contoh lain adalah metode
.toString()
untuk semua objek JS. Kita dapat menulis metode
.toString()
untuk setiap kelas yang kita buat. Selanjutnya, Anda bisa membuat dua kelas yang mengimplementasikan metode
.toString()
berbeda. Kami akan menamai mereka
Left
dan
Right
(sedikit kemudian saya akan menjelaskan nama-nama).
class Left { constructor(val) { this._val = val; } toString() { const str = this._val.toString(); return `Left(${str})`; } }
class Right { constructor(val) { this._val = val; } toString() { const str = this._val.toString(); return `Right(${str})`; } }
Sekarang buat fungsi yang memanggil
.toString()
pada dua objek ini:
function trace(val) { console.log(val.toString()); return val; } trace(new Left('Hello world'));
Bukan kode yang luar biasa, saya tahu. Tetapi kenyataannya adalah kita memiliki dua tipe perilaku berbeda yang menggunakan antarmuka yang sama. Ini adalah polimorfisme. Tapi perhatikan sesuatu yang menarik. Berapa banyak jika pernyataan yang kita gunakan? Nol Bukan satu pun. Kami menciptakan dua jenis perilaku berbeda tanpa pernyataan if tunggal. Mungkin sesuatu seperti ini dapat digunakan untuk menangani kesalahan ...
Kiri dan Kanan
Kembali ke masalah kita. Kita perlu menentukan jalur yang berhasil dan tidak berhasil untuk kode kita. Di jalur yang baik, kita cukup menjalankan kode dengan tenang sampai kesalahan terjadi atau kita menyelesaikannya. Jika kita menemukan diri kita di jalur yang salah, kita tidak akan lagi mencoba menjalankan kode. Kita bisa memberi nama jalur ini Happy and Sad, tetapi coba ikuti konvensi penamaan yang digunakan bahasa dan pustaka pemrograman lain. Jadi, mari kita sebut jalur buruk Kiri, dan yang sukses - Kanan.
Mari kita membuat metode yang menjalankan fungsi jika kita berada di jalur yang baik, tetapi abaikan itu di jalur yang buruk:
class Left { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath() {
class Right { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath(fn) { return fn(this._val); } toString() { const str = this._val.toString(); return `Right(${str})`; } }
Sesuatu seperti ini:
const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); leftHello.runFunctionOnlyOnHappyPath(trace);
Siaran
Kami sedang mendekati sesuatu yang bermanfaat, tetapi belum cukup.
.runFunctionOnlyOnHappyPath()
mengembalikan properti
_val
. Semuanya baik-baik saja, tetapi terlalu merepotkan jika kita ingin menjalankan lebih dari satu fungsi. Mengapa Karena kita tidak lagi tahu apakah kita berada di jalan yang benar atau salah. Informasi menghilang segera setelah kami mengambil nilai di luar Kiri dan Kanan. Jadi yang bisa kita lakukan adalah mengembalikan jalur Kiri atau Kanan dengan
_val
baru di dalamnya. Dan kami akan mempersingkat namanya, karena kami ada di sini. Apa yang kita lakukan adalah menerjemahkan fungsi dari dunia nilai sederhana ke dunia Kiri dan Kanan. Karena itu, kami memanggil metode
map()
:
class Left { constructor(val) { this._val = val; } map() {
class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } toString() { const str = this._val.toString(); return `Right(${str})`; } }
Kami memasukkan metode ini dan menggunakan Kiri atau Kanan dalam sintaks gratis:
const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); const helloToGreetings = str => str.replace(/Hello/, 'Greetings,'); leftHello.map(helloToGreetings).map(trace);
Kami telah membuat dua jalur eksekusi. Kita dapat meletakkan data di jalur yang sukses dengan memanggil
new Right()
, atau pada path gagal dengan memanggil
new Left()
.
Setiap kelas mewakili jalur: berhasil atau tidak berhasil. Saya mencuri metafora kereta api ini dari Scott VlaschinaJika
map
bekerja di jalur yang baik, ikuti dan proses data. Jika kita mendapati diri kita tidak berhasil, tidak ada yang akan terjadi. Terus berikan nilainya lebih lanjut. Jika kita, misalnya, menempatkan Kesalahan pada jalur yang gagal ini, kita akan mendapatkan sesuatu yang sangat mirip dengan
try…catch
.
Gunakan .map()
untuk bergerak di sepanjang jalanSaat Anda maju, menjadi sedikit sulit sepanjang waktu untuk menulis Kiri atau Kanan, jadi mari kita sebut kombinasi ini hanya Entah (“baik”). Baik kiri atau kanan.
Cara pintas untuk membuat objek
Jadi, langkah selanjutnya adalah menulis ulang fungsi contoh kita sehingga kembali. Kiri untuk kesalahan atau Kanan untuk nilai. Tetapi sebelum kita melakukan ini, bersenang-senanglah. Mari kita menulis beberapa cara pintas. Yang pertama adalah metode statis yang disebut
.of()
. Itu hanya mengembalikan Kiri atau Kanan baru. Kode mungkin terlihat seperti ini:
Left.of = function of(x) { return new Left(x); }; Right.of = function of(x) { return new Right(x); };
Jujur, bahkan
Left.of()
dan
Right.of()
membosankan untuk ditulis. Jadi saya condong ke arah label
left()
dan
right()
bahkan lebih pendek:
function left(x) { return Left.of(x); } function right(x) { return Right.of(x); }
Dengan pintasan ini, kita mulai menulis ulang fungsi aplikasi:
function zipRow(headerFields, fieldData) { const lengthMatch = (headerFields.length == fieldData.length); return (!lengthMatch) ? left(new Error("Row has an unexpected number of fields")) : right(_.zipObject(headerFields, fieldData)); } function addDateStr(messageObj) { const errMsg = 'Unable to parse date stamp in message object'; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const d = new Date(messageObj.datestamp); if (isNaN(d)) { return left(new Error(errMsg)); } const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`; return right({datestr, ...messageObj}); }
Fungsi yang dimodifikasi tidak jauh berbeda dari yang lama. Kami cukup membungkus nilai kembali dalam Kiri atau Kanan, tergantung pada apakah ada kesalahan.
Setelah itu, kita dapat mulai memproses fungsi utama yang memproses satu baris. Untuk memulainya, masukkan string ke Either with
right()
, lalu terjemahkan
splitFields
untuk membaginya:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields);
Ini berfungsi dengan baik, tetapi masalah terjadi jika Anda mencoba melakukan hal yang sama dengan
zipRow()
:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow );
Faktanya adalah
zipRow()
mengharapkan dua parameter. Tetapi fungsi yang kami berikan ke
.map()
hanya mendapatkan satu nilai dari properti
._val
. Situasi ini dapat diperbaiki menggunakan versi
zipRow()
. Mungkin terlihat seperti ini:
function zipRow(headerFields) { return function zipRowWithHeaderFields(fieldData) { const lengthMatch = (headerFields.length == fieldData.length); return (!lengthMatch) ? left(new Error("Row has an unexpected number of fields")) : right(_.zipObject(headerFields, fieldData)); }; }
Perubahan kecil ini menyederhanakan konversi
zipRow
, sehingga akan bekerja dengan baik dengan
.map()
:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields));
Bergabunglah
Menggunakan
.map()
untuk menjalankan
splitFields()
tidak masalah, karena
.splitFields()
tidak mengembalikan Either. Tetapi ketika Anda harus menjalankan
zipRow()
, masalah muncul karena ia akan kembali. Jadi, ketika menggunakan
.map()
kita berakhir dengan Either inside Either. Jika kita melangkah lebih jauh, macet sampai kita menjalankan
.map()
di dalam
.map()
. Itu juga tidak akan berhasil. Kita perlu beberapa cara untuk menggabungkan ini. Jadi mari kita menulis metode baru, yang akan kita sebut
.join()
:
class Left { constructor(val) { this._val = val; } map() {
class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } join() { if ((this._val instanceof Left) || (this._val instanceof Right)) { return this._val; } return this; } toString() { const str = this._val.toString(); return `Right(${str})`; } }
Sekarang kita dapat "membuka" aset kita:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields)).join(); const rowObjWithDate = rowObj.map(addDateStr).join();
Rantai
Kami telah menempuh perjalanan panjang. Tapi Anda harus ingat panggilan
.join()
sepanjang waktu, yang menjengkelkan. Namun, kami memiliki pola panggilan
.map()
dan
.join()
berurutan umum, jadi mari kita buat metode akses cepat untuknya. Sebut saja
chain()
, karena ia mengikat fungsi yang mengembalikan Kiri atau Kanan.
class Left { constructor(val) { this._val = val; } map() {
class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } join() { if ((this._val instanceof Left) || (this._val instanceof Right)) { return this._val; } return this; } chain(fn) { return fn(this._val); } toString() { const str = this._val.toString(); return `Right(${str})`; } }
Kembali ke analogi kereta api,
.chain()
mengganti rel jika kita menemukan kesalahan. Namun, lebih mudah ditampilkan pada diagram.
Jika terjadi kesalahan, metode .chain () memungkinkan Anda untuk beralih ke jalur kiri. Harap dicatat bahwa sakelar hanya bekerja satu arah.Kode mendapat pembersih sedikit:
function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr);
Lakukan sesuatu dengan nilai
Refactoring fungsi
processRow()
hampir selesai. Tetapi apa yang terjadi ketika kita mengembalikan nilainya? Pada akhirnya, kami ingin mengambil tindakan berbeda tergantung pada situasi seperti apa yang kami miliki: Kiri atau Kanan. Karena itu, kami akan menulis fungsi yang akan mengambil tindakan yang sesuai:
function either(leftFunc, rightFunc, e) { return (e instanceof Left) ? leftFunc(e._val) : rightFunc(e._val); }
Saya menipu dan menggunakan nilai internal objek Kiri atau Kanan. Tetapi berpura-puralah Anda tidak memperhatikan hal ini. Sekarang kita dapat menyelesaikan fungsi kita: function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); }
Dan jika kita merasa sangat pintar, maka kita dapat kembali menggunakan sintaks gratis: function processRow(headerFields, row) { const rowObjWithDate = right(row) .map(splitFields) .chain(zipRow(headerFields)) .chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); }
Kedua versi ini cukup cantik. Tidak ada desain try...catch
. Dan tidak ada pernyataan if di fungsi level atas. Jika ada masalah dengan saluran tertentu, kami cukup menampilkan pesan kesalahan di bagian akhir. Dan perhatikan bahwa processRow()
kami menyebutkan Kiri atau Kanan satu-satunya waktu di awal ketika kami menelepon right()
. Sisanya hanya metode yang digunakan .map()
dan .chain()
untuk penggunaan fungsi berikutnya.ap dan angkat
Kelihatannya bagus, tetapi masih mempertimbangkan satu skenario terakhir. Mengikuti contoh kami, mari kita lihat bagaimana Anda dapat memproses semua data CSV, bukan hanya setiap baris secara individual. Kami akan membutuhkan fungsi bantu (pembantu) atau tiga: function splitCSVToRows(csvData) {
Jadi, kami memiliki pembantu yang memecah CSV menjadi beberapa baris. Dan kami kembali ke opsi dengan Either. Sekarang Anda dapat menggunakan .map()
beberapa fungsi lodash untuk mengekstrak bilah judul dari baris data. Tapi kami menemukan diri kami dalam situasi yang menarik ... function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail);
Kami memiliki bidang tajuk dan baris data yang siap ditampilkan processRows()
. Tapi headerFields
juga dataRows
dibungkus Either. Kami membutuhkan beberapa cara untuk mengonversi processRows()
ke fungsi yang berfungsi dengan Baik. Untuk memulai, kami melakukan kari processRows
. function processRows(headerFields) { return function processRowsWithHeaderFields(dataRows) {
Sekarang semuanya siap untuk percobaan. Kami memiliki headerFields
, yang manapun, melilit array. Apa yang akan terjadi jika kita mengambil headerFields
dan panggilan itu .map()
ke processRows()
? function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail);
Dengan .map (), fungsi eksternal disebut di sini processRows()
, tetapi bukan yang internal. Dengan kata lain, processRows()
mengembalikan fungsi. Dan sejak itu .map()
, kami masih mendapatkan kembali. Dengan demikian, hasilnya adalah fungsi di dalam Either, yang disebut funcInEither
. Dibutuhkan array string dan mengembalikan array string lain. Kita perlu mengambil fungsi ini dan menyebutnya dengan nilai di dalamnya dataRows
. Untuk melakukan ini, tambahkan metode lain ke kelas kami Kiri dan Kanan. Kami akan menyebutnya .ap()
sesuai dengan standar .Seperti biasa, metode ini tidak melakukan apa pun di trek Kiri:
Dan untuk kelas yang Tepat, kami mengharapkan yang lain dengan fungsi:
Sekarang kita dapat menyelesaikan fungsi utama kami: function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); const funcInEither = headerFields.map(processRows); const messagesArr = dataRows.ap(funcInEither); return either(showError, showMessages, messagesArr); }
Inti dari metode ini .ap()
segera dipahami sedikit (spesifikasi Fantasy Land membingungkannya, tetapi dalam kebanyakan bahasa lain metode ini digunakan sebaliknya). Jika Anda menggambarkannya dengan lebih mudah, maka Anda berkata: “Saya memiliki fungsi yang biasanya membutuhkan dua nilai sederhana. Saya ingin mengubahnya menjadi fungsi yang membutuhkan dua Either. " Jika tersedia, .ap()
kita dapat menulis fungsi yang akan melakukan hal itu. Sebut saja liftA2()
, lagi sesuai dengan nama standar. Dia mengambil fungsi sederhana yang mengharapkan dua argumen, dan "mengangkat" untuk bekerja dengan "pelamar". (Ini adalah objek yang mengandung metode .ap()
dan metode .of()
). Jadi, liftA2 adalah kependekan dari "lift aplikatif, dua parameter".Jadi suatu fungsi liftA2
mungkin terlihat seperti ini: function liftA2(func) { return function runApplicativeFunc(a, b) { return b.ap(a.map(func)); }; }
Fungsi tingkat atas kami akan menggunakannya sebagai berikut: function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); const processRowsA = liftA2(processRows); const messagesArr = processRowsA(headerFields, dataRows); return either(showError, showMessages, messagesArr); }
Kode pada CodePen .Benar? Hanya itu semua
Anda mungkin bertanya, apa yang lebih baik dari pengecualian sederhana? Apakah menurut saya ini bukan solusi yang rumit untuk masalah sederhana? Mari kita pikirkan mengapa kita menyukai pengecualian. Jika tidak ada pengecualian, Anda harus menulis banyak pernyataan if di mana-mana. Kami akan selalu menulis kode sesuai dengan prinsip "jika yang terakhir bekerja, lanjutkan, jika tidak, proses kesalahan." Dan kita harus menangani kesalahan ini di seluruh kode. Ini membuatnya sulit untuk memahami apa yang terjadi. Pengecualian memungkinkan Anda untuk keluar dari program jika terjadi kesalahan. Karena itu, Anda tidak perlu menulis semua jika ini. Anda dapat fokus pada jalur eksekusi yang sukses.Tapi ada satu halangan. Pengecualian menyembunyikan terlalu banyak. Saat Anda melempar pengecualian, Anda mentransfer masalah penanganan kesalahan ke beberapa fungsi lainnya. Terlalu mudah untuk mengabaikan pengecualian yang akan muncul ke level tertinggi. Sisi baik dari Either adalah memungkinkan Anda untuk melompat keluar dari aliran program utama, seolah-olah dengan pengecualian. Dan itu bekerja dengan jujur. Anda bisa Kanan atau Kiri. Anda tidak dapat berpura-pura bahwa opsi Kiri tidak mungkin. Pada akhirnya, Anda harus menarik nilainya dengan panggilan seperti either()
.Saya tahu ini terdengar seperti semacam kompleksitas. Tetapi lihat kode yang kami tulis (bukan kelas, tetapi fungsi yang menggunakannya). Tidak ada banyak pengecualian dalam menangani kode. Hampir tidak ada, kecuali panggilan either()
di akhir csvToMessages()
danprocessRow()
. Itulah intinya. Dengan Either, Anda memiliki penanganan kesalahan bersih yang tidak bisa dilupakan secara tidak sengaja. Tanpa Either, cap melalui kode dan tambahkan padding di mana-mana.Ini tidak berarti bahwa Anda tidak boleh menggunakannya try...catch
. Terkadang ini alat yang tepat, dan itu normal. Tapi ini bukan satu-satunya alat. Entah memberi Anda beberapa manfaat yang tidak Anda miliki try...catch
. Jadi beri kesempatan pada monad ini. Meskipun awalnya sulit, saya pikir Anda akan menyukainya. Tolong, tolong jangan gunakan implementasi dari artikel ini. Cobalah salah satu perpustakaan terkenal seperti Crocks , Sanctuary , Folktale atau Monet . Mereka lebih baik dilayani. Dan di sini, untuk kesederhanaan, saya melewatkan sesuatu.Sumber Daya Tambahan