Belum lama ini saya mulai menggunakan Scala, bukan Java. Bagian dari proses "konversi" antara bahasa untuk saya adalah belajar menggunakan Either
s bukan (dicentang) Exception
s. Saya telah mengkode cara ini untuk sementara waktu, tetapi baru-baru ini saya mulai bertanya-tanya apakah itu benar-benar cara yang lebih baik untuk pergi.
Salah satu keunggulan utama Either
telah berakhir Exception
adalah kinerja yang lebih baik; sebuah Exception
kebutuhan untuk membangun tumpukan-jejak dan sedang dilemparkan. Sejauh yang saya mengerti, melempar Exception
bukan bagian yang menuntut, tetapi membangun stack-trace adalah.
Tapi kemudian, kita selalu bisa membuat / mewarisi Exception
dengan scala.util.control.NoStackTrace
, dan lebih dari itu, saya melihat banyak kasus di mana sisi kiri sebuah Either
sebenarnya adalah Exception
(meneruskan peningkatan kinerja).
Satu keuntungan lain yang dimiliki Either
adalah keamanan kompiler; kompiler Scala tidak akan mengeluh tentang yang tidak ditangani Exception
(tidak seperti kompiler Java). Tetapi jika saya tidak salah, keputusan ini beralasan dengan alasan yang sama yang sedang dibahas dalam topik ini, jadi ...
Dalam hal sintaks, saya merasa seperti- Exception
gaya jauh lebih jelas. Periksa blok kode berikut (keduanya mencapai fungsi yang sama):
Either
gaya:
def compute(): Either[String, Int] = {
val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")
val bEithers: Iterable[Either[String, Int]] = someSeq.map {
item => if (someCondition(item)) Right(item.toInt) else Left("bad")
}
for {
a <- aEither.right
bs <- reduce(bEithers).right
ignore <- validate(bs).right
} yield compute(a, bs)
}
def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code
def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()
def compute(a: String, bs: Iterable[Int]): Int = ???
Exception
gaya:
@throws(classOf[ComputationException])
def compute(): Int = {
val a = if (someCondition) "good" else throw new ComputationException("bad")
val bs = someSeq.map {
item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
}
if (bs.sum > 22) throw new ComputationException("bad")
compute(a, bs)
}
def compute(a: String, bs: Iterable[Int]): Int = ???
Yang terakhir terlihat jauh lebih bersih bagi saya, dan kode yang menangani kegagalan (baik pencocokan pola Either
atau try-catch
) cukup jelas dalam kedua kasus.
Jadi pertanyaan saya adalah - mengapa menggunakan Either
lebih dari (dicentang) Exception
?
Memperbarui
Setelah membaca jawaban, saya menyadari bahwa saya mungkin gagal menyajikan inti dari dilema saya. Kekhawatiran saya bukan pada kurangnya try-catch
; seseorang dapat "menangkap" Exception
dengan Try
, atau menggunakan catch
untuk membungkus pengecualian Left
.
Masalah utama saya dengan Either
/ Try
muncul ketika saya menulis kode yang mungkin gagal di banyak titik di sepanjang jalan; dalam skenario ini, ketika menemui kegagalan, saya harus menyebarkan kegagalan itu di seluruh kode saya, sehingga membuat kode cara yang lebih rumit (seperti yang ditunjukkan dalam contoh-contoh tersebut).
Sebenarnya ada cara lain untuk memecahkan kode tanpa Exception
menggunakan return
(yang sebenarnya adalah "tabu" di Scala). Kode masih lebih jelas daripada Either
pendekatan, dan sementara menjadi sedikit kurang bersih daripada Exception
gaya, tidak akan ada rasa takut dari yang tidak tertangkap Exception
.
def compute(): Either[String, Int] = {
val a = if (someCondition) "good" else return Left("bad")
val bs: Iterable[Int] = someSeq.map {
item => if (someCondition(item)) item.toInt else return Left("bad")
}
if (bs.sum > 22) return Left("bad")
val c = computeC(bs).rightOrReturn(return _)
Right(computeAll(a, bs, c))
}
def computeC(bs: Iterable[Int]): Either[String, Int] = ???
def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???
implicit class ConvertEither[L, R](either: Either[L, R]) {
def rightOrReturn(f: (Left[L, R]) => R): R = either match {
case Right(r) => r
case Left(l) => f(Left(l))
}
}
Pada dasarnya return Left
penggantian throw new Exception
, dan metode implisit pada keduanya rightOrReturn
, adalah suplemen untuk propagasi pengecualian otomatis di stack.
sumber
Try
. Bagian tentangEither
vsException
hanya menyatakan bahwaEither
s harus digunakan ketika kasus lain dari metode ini adalah "tidak luar biasa". Pertama, ini adalah definisi imho yang sangat, sangat kabur. Kedua, apakah itu sepadan dengan hukuman sintaksis? Maksudku, aku benar-benar tidak akan keberatan menggunakanEither
s jika bukan karena overhead sintaks yang mereka sajikan.Either
Sepertinya monad bagiku. Gunakan saat Anda membutuhkan manfaat komposisi fungsional yang disediakan monad. Atau mungkin juga tidak .Either
dengan sendirinya bukan monad. The proyeksi baik sisi kiri atau sisi kanan adalah monad, tapiEither
dengan sendirinya tidak. Anda dapat menjadikannya monad, dengan "membiaskan" ke sisi kiri atau kanan. Namun, kemudian Anda memberikan semantik tertentu di kedua sisiEither
. ScalaEither
awalnya tidak bias, tetapi bias agak baru-baru ini, sehingga saat ini, itu sebenarnya sebuah monad, tetapi "kebesaran" bukan properti yang melekatEither
melainkan hasil dari itu menjadi bias.Jawaban:
Jika Anda hanya menggunakan blok
Either
imperatif persis seperti itutry-catch
, tentu saja itu akan terlihat seperti sintaks yang berbeda untuk melakukan hal yang persis sama.Eithers
adalah sebuah nilai . Mereka tidak memiliki batasan yang sama. Kamu bisa:Eithers
tidak harus berada tepat di sebelah bagian "coba". Bahkan dapat dibagikan dalam fungsi umum. Ini membuatnya lebih mudah untuk memisahkan masalah dengan benar.Eithers
dalam struktur data. Anda dapat mengulanginya dan menggabungkan pesan kesalahan, menemukan pesan pertama yang tidak gagal, dengan malas mengevaluasinya, atau serupa.Eithers
? Anda dapat menulis fungsi pembantu untuk membuatnya lebih mudah digunakan untuk kasus penggunaan khusus Anda. Tidak seperti try-catch, Anda tidak terjebak secara permanen dengan hanya antarmuka default yang dihasilkan oleh perancang bahasa.Catatan untuk sebagian besar kasus penggunaan,
Try
memiliki antarmuka yang lebih sederhana, memiliki sebagian besar manfaat di atas, dan biasanya memenuhi kebutuhan Anda juga. AnOption
bahkan lebih sederhana jika yang Anda pedulikan adalah kesuksesan atau kegagalan dan tidak memerlukan informasi status seperti pesan kesalahan tentang kasus kegagalan.Dalam pengalaman saya,
Eithers
tidak benar-benar disukai daripadaTrys
kecuali ituLeft
adalah sesuatu selainException
, mungkin pesan kesalahan validasi, dan Anda ingin mengagregasikanLeft
nilainya. Misalnya, Anda sedang memproses beberapa input dan Anda ingin mengumpulkan semua pesan kesalahan, bukan hanya salah pada yang pertama. Situasi itu tidak sering muncul dalam praktik, setidaknya untuk saya.Lihatlah di luar model try-catch dan Anda akan menemukan
Eithers
jauh lebih menyenangkan untuk dikerjakan.Pembaruan: pertanyaan ini masih mendapatkan perhatian, dan saya belum melihat pembaruan OP mengklarifikasi pertanyaan sintaks, jadi saya pikir saya akan menambahkan lebih banyak.
Sebagai contoh untuk keluar dari pola pikir pengecualian, ini adalah bagaimana saya biasanya menulis kode contoh Anda:
Di sini Anda dapat melihat bahwa semantik
filterOrElse
menangkap dengan sempurna apa yang terutama kami lakukan dalam fungsi ini: memeriksa kondisi dan mengaitkan pesan kesalahan dengan kegagalan yang diberikan. Karena ini adalah kasus penggunaan yang sangat umum,filterOrElse
ada di perpustakaan standar, tetapi jika tidak, Anda bisa membuatnya.Ini adalah titik di mana seseorang menghasilkan contoh tandingan yang tidak dapat dipecahkan dengan cara yang persis sama dengan saya, tetapi poin yang saya coba buat adalah tidak hanya ada satu cara untuk digunakan
Eithers
, tidak seperti pengecualian. Ada beberapa cara untuk menyederhanakan kode untuk sebagian besar situasi penanganan kesalahan, jika Anda menjelajahi semua fungsi yang tersedia untuk digunakanEithers
dan terbuka untuk eksperimen.sumber
try
dancatch
merupakan argumen yang adil, tetapi tidakkah ini bisa dengan mudah dicapaiTry
? 2. MenyimpanEither
dalam struktur data dapat dilakukan di sampingthrows
mekanisme; bukankah itu hanya lapisan tambahan di atas penanganan kegagalan? 3. Anda pasti dapat memperpanjangException
s. Tentu, Anda tidak dapat mengubahtry-catch
struktur, tetapi menangani kegagalan adalah hal yang sangat umum sehingga saya pasti ingin bahasanya memberi saya boilerplate untuk itu (yang saya tidak dipaksa untuk menggunakan). Itu seperti mengatakan bahwa untuk-pemahaman adalah buruk karena itu adalah boiler untuk operasi monadik.Either
s dapat diperpanjang, di mana jelas mereka tidak bisa (menjadisealed trait
dengan hanya dua implementasi, keduanyafinal
). Dapatkah Anda menguraikan itu? Saya berasumsi Anda tidak bermaksud arti harfiah memperluas.Keduanya sebenarnya sangat berbeda.
Either
Semantik jauh lebih umum daripada hanya untuk mewakili perhitungan yang berpotensi gagal.Either
memungkinkan Anda mengembalikan salah satu dari dua jenis yang berbeda. Itu dia.Sekarang, salah satu dari dua jenis itu mungkin atau mungkin tidak mewakili kesalahan dan yang lain mungkin atau mungkin tidak mewakili keberhasilan, tetapi itu hanya satu kemungkinan kasus penggunaan dari banyak jenis.
Either
jauh, jauh lebih umum dari itu. Anda bisa saja memiliki metode yang membaca input dari pengguna yang diizinkan memasukkan nama atau tanggal mengembalikan suatuEither[String, Date]
.Atau, pikirkan tentang lambda literal dalam C♯: mereka mengevaluasi ke
Func
atauExpression
, yaitu mereka mengevaluasi ke fungsi anonim atau mereka mengevaluasi ke AST. Dalam C♯, ini terjadi "secara ajaib", tetapi jika Anda ingin memberikan jenis yang tepat untuk itu, itu akan menjadiEither<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>
. Namun, tak satu pun dari kedua opsi tersebut merupakan kesalahan, dan tak satu pun dari keduanya adalah "normal" atau "luar biasa". Mereka hanya dua jenis berbeda yang dapat dikembalikan dari fungsi yang sama (atau dalam hal ini, dianggap berasal dari literal yang sama). [Catatan: sebenarnya,Func
harus dipecah menjadi dua kasus lain, karena C♯ tidak memilikiUnit
tipe, dan sesuatu yang tidak mengembalikan apa pun tidak dapat direpresentasikan dengan cara yang sama dengan sesuatu yang mengembalikan sesuatu,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>
Sekarang,
Either
dapat digunakan untuk mewakili tipe kesuksesan dan tipe kegagalan. Dan jika Anda setuju pada pemesanan yang konsisten dari kedua jenis, Anda bahkan dapat biasEither
untuk memilih salah satu dari dua jenis, sehingga memungkinkan untuk menyebarkan kegagalan melalui rantai monadic. Tetapi dalam kasus itu, Anda memberikan semantik tertentu keEither
yang tidak harus dimiliki sendiri.An
Either
yang memiliki salah satu dari dua jenisnya ditetapkan menjadi tipe kesalahan dan bias ke satu sisi untuk menyebarkan kesalahan, biasanya disebutError
monad, dan akan menjadi pilihan yang lebih baik untuk mewakili perhitungan yang berpotensi gagal. Dalam Scala, jenis ini disebutTry
.Lebih masuk akal membandingkan penggunaan pengecualian dengan
Try
denganEither
.Jadi, ini adalah alasan # 1 mengapa
Either
mungkin digunakan lebih dari pengecualian yang diperiksa:Either
jauh lebih umum daripada pengecualian. Ini dapat digunakan dalam kasus di mana pengecualian bahkan tidak berlaku.Namun, Anda kemungkinan besar bertanya tentang kasus terbatas di mana Anda hanya menggunakan
Either
(atau lebih mungkinTry
) sebagai pengganti pengecualian. Dalam hal ini, masih ada beberapa keuntungan, atau pendekatan yang agak berbeda mungkin terjadi.Keuntungan utama adalah bahwa Anda dapat menunda penanganan kesalahan. Anda hanya menyimpan nilai pengembalian, bahkan tanpa melihat apakah itu merupakan keberhasilan atau kesalahan. (Tangani nanti.) Atau berikan di tempat lain. (Biarkan orang lain mengkhawatirkannya.) Kumpulkan mereka dalam daftar. (Tangani semuanya sekaligus). Anda dapat menggunakan rantai monadik untuk menyebarkannya di sepanjang pipa pemrosesan.
Semantik perkecualian tertanam dalam bahasa tersebut. Di Scala, pengecualian melepaskan tumpukan, misalnya. Jika Anda membiarkannya menggembung ke handler pengecualian, maka handler tidak bisa kembali ke tempat kejadiannya dan memperbaikinya. OTOH, jika Anda menangkapnya lebih rendah di tumpukan sebelum melepaskannya dan merusak konteks yang diperlukan untuk memperbaikinya, maka Anda mungkin berada di komponen yang terlalu rendah untuk membuat keputusan yang terinformasi tentang bagaimana untuk melanjutkan.
Hal ini berbeda dalam Smalltalk, misalnya, di mana pengecualian tidak bersantai stack dan resumable, dan Anda dapat menangkap pengecualian tinggi di tumpukan (mungkin sebagai tinggi-tinggi sebagai debugger), memperbaiki kesalahan waaaaayyyyy bawah di susun dan lanjutkan eksekusi dari sana. Atau kondisi CommonLisp di mana mengangkat, menangkap, dan menangani adalah tiga hal yang berbeda alih-alih dua (penanganan mengangkat dan menangkap) seperti halnya pengecualian.
Menggunakan tipe pengembalian kesalahan yang sebenarnya alih-alih pengecualian memungkinkan Anda melakukan hal serupa, dan lebih banyak lagi, tanpa harus memodifikasi bahasa.
Dan kemudian ada gajah yang jelas di ruangan: pengecualian adalah efek samping. Efek sampingnya jahat. Catatan: Saya tidak mengatakan bahwa ini benar. Tetapi ini adalah sudut pandang yang dimiliki beberapa orang, dan dalam hal ini, keuntungan dari jenis kesalahan lebih dari pengecualian jelas. Jadi, jika Anda ingin melakukan pemrograman fungsional, Anda mungkin menggunakan tipe kesalahan karena satu-satunya alasan bahwa mereka adalah pendekatan fungsional. (Perhatikan bahwa Haskell memiliki pengecualian. Perhatikan juga bahwa tidak ada yang suka menggunakannya.)
sumber
Exception
.Either
sepertinya alat yang hebat, tapi ya, saya secara khusus bertanya tentang menangani kegagalan, dan saya lebih suka menggunakan alat yang jauh lebih spesifik untuk tugas saya daripada yang sangat kuat. Menunda penanganan kegagalan adalah argumen yang adil, tetapi orang dapat dengan mudah menggunakanTry
metode melemparException
jika dia ingin tidak menanganinya saat ini. Apakah stack-trace unwinding mempengaruhiEither
/Try
juga? Anda dapat mengulangi kesalahan yang sama ketika menyebarkannya secara tidak benar; mengetahui kapan menangani kegagalan bukan hal sepele dalam kedua kasus tersebut.Yang menyenangkan tentang pengecualian adalah bahwa kode asal telah mengklasifikasikan keadaan luar biasa untuk Anda. Perbedaan antara
IllegalStateException
danBadParameterException
lebih mudah dibedakan dalam bahasa yang diketik lebih kuat. Jika Anda mencoba menguraikan "baik" dan "buruk", Anda tidak memanfaatkan alat yang sangat kuat yang Anda inginkan, yaitu kompiler Scala. Ekstensi Anda sendiri untukThrowable
dapat memasukkan informasi sebanyak yang Anda butuhkan, di samping string pesan teks.Ini adalah utilitas sebenarnya
Try
dalam lingkungan Scala, denganThrowable
bertindak sebagai pembungkus item kiri untuk membuat hidup lebih mudah.sumber
Either
memiliki masalah gaya karena itu bukan monad sejati (?); kesewenang-wenangan darileft
nilai menjauhi nilai tambah yang lebih tinggi ketika Anda melampirkan informasi tentang kondisi luar biasa dalam struktur yang tepat. Jadi membandingkan penggunaanEither
dalam satu kasus mengembalikan string di sisi kiri, dibandingkantry/catch
dalam kasus lain menggunakan diketik dengan benarThrowables
tidak tepat. Karena nilaiThrowables
untuk memberikan informasi, dan cara ringkas yangTry
menanganiThrowables
,Try
adalah taruhan yang lebih baik daripada ...Either
atautry/catch
try-catch
. Seperti yang dikatakan dalam pertanyaan yang terakhir terlihat jauh lebih bersih bagi saya, dan kode yang menangani kegagalan (baik pencocokan pola pada Either atau try-catch) cukup jelas dalam kedua kasus.