Neden Her ikisi de (işaretli) İstisna kullanılır?


17

Çok uzun zaman önce Java yerine Scala kullanmaya başladım. Benim için diller arasındaki "dönüşüm" sürecinin bir Eitherkısmı (işaretli) yerine s kullanmayı öğreniyordu Exception. Bir süredir bu şekilde kod yazıyorum, ancak son zamanlarda bunun gerçekten daha iyi bir yol olup olmadığını merak etmeye başladım.

Önemli bir avantaj Eitheredemediği Exceptiondaha iyi performans; bir Exceptionyığın izi oluşturmak için bir ihtiyaç ve atıldı. Anladığım kadarıyla, atmak Exceptionzor kısım değil, yığın izini oluşturmak.

Ama sonra, bir zaman / devralır inşa edebilir Exceptionler ile scala.util.control.NoStackTraceve bir sol tarafı nerede daha çok, ben durumlarda bol bkz Eitheraslında bir olduğunu Exception(performans artışı forgoing).

Bir diğer avantajı Eitherda derleyici güvenliği; Scala derleyicisi, işlenmeyenlerden Exception(Java derleyicisinin aksine) şikayet etmeyecektir . Ancak yanılmıyorsam, bu karar, bu konuda tartışılan aynı akıl ile gerekçelendirilir, bu yüzden ...

Sözdizimi açısından, Exception-style çok daha net gibi hissediyorum . Aşağıdaki kod bloklarını inceleyin (her ikisi de aynı işlevi görür):

Either stili:

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 stili:

@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 = ???

İkincisi bana çok daha temiz görünüyor ve hatayı işleyen kod (her iki durumda da desen eşleştirme Eitherveya try-catch) oldukça açık.

Benim sorum şu - neden kullanım Either(işaretli) Exception?

Güncelleme

Cevapları okuduktan sonra ikilemin özünü sunamayabilirim. Benim endişe değil eksikliği try-catch; bir ya "yakalama" bir can Exceptionile Tryveya kullanmak catchile istisna sarmak için Left.

Ana sorun Either/ Tryyol boyunca birçok noktada başarısız olabilir kod yazarken gelir; bu senaryolarda, bir hatayla karşılaştığımda, bu hatayı tüm kodum boyunca yaymak zorunda kaldım, böylece kodu (yukarıda belirtilen örneklerde gösterildiği gibi) daha hantal hale getirmeliyim.

Aslında kodu Exceptionkullanarak s olmadan kırmanın başka bir yolu var (aslında returnScala'da başka bir "tabu"). Kod hala Eitheryaklaşımdan daha açık ve Exceptionstilden biraz daha az temiz olsa da, yakalanmayanlardan korkmayacaktı 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))
  }
}

Temel olarak, return Leftikame throw new Exceptionve ikisinin üzerindeki örtük yöntem rightOrReturn, yığının otomatik istisnasının yayılması için bir tamamlayıcıdır.



@RobertHarvey Makalenin çoğu tartışıyor gibi görünüyor Try. EitherVs ile ilgili bölüm Exceptionsadece Eitheryöntemin diğer örneği "istisnai değildir" olduğunda kullanılması gerektiğini belirtir . Birincisi, bu çok ama çok belirsiz bir tanım imho. İkincisi, sözdizimi cezasına gerçekten değer mi? Demek istediğim, Eithereğer sundukları sözdizimi yükü olmasaydı, s'yi kullanmakta sorun olmaz.
Eyal Roth

Eitherbana bir monad gibi geliyor. Monadların sağladığı fonksiyonel kompozisyon avantajlarına ihtiyacınız olduğunda kullanın. Ya da, belki değil .
Robert Harvey

2
@RobertHarvey: Teknik olarak konuşmak gerekirse, Eithertek başına bir monad değil. Çıkıntı sol tarafında veya sağ tarafa bir tek hücreli, ama Eithertek başına değil. Sen edebilir hale olsa sol tarafta veya sağ tarafa onu "polarizasyon" tarafından, bir monad. Ancak, o zaman her iki tarafına belirli bir semantik verirsiniz Either. Scala'nın Eitherbaşlangıçta tarafsız olduğu, ancak son zamanlarda önyargılı olduğu için, bugünlerde aslında bir monad, ancak "monadness", doğasında var olan bir özellik Eitherdeğil, bunun önyargılı olmasının bir sonucudur.
Jörg W Mittag

@ JörgWMittag: Güzel. Bu, tüm monadik iyiliği için kullanabileceğiniz ve özellikle ortaya çıkan kodun ne kadar "temiz" olmasından rahatsız olmayacağınız anlamına gelir (işlevsel işlev devam kodunun gerçekten İstisna sürümünden daha temiz olacağından şüpheleniyorum).
Robert Harvey

Yanıtlar:


13

Sadece Eithertam olarak zorunlu bir try-catchblok kullanırsanız, elbette aynı şeyi yapmak için farklı bir sözdizimine benzeyecektir. Eithersbir değerdir . Aynı sınırlamalara sahip değiller. Yapabilirsin:

  • Deneme bloklarınızı küçük ve bitişik tutma alışkanlığınızı durdurun. "Catch" kısmının Eithers"try" kısmının hemen yanında olması gerekmez. Hatta ortak bir işlevde paylaşılabilir. Bu, endişeleri doğru bir şekilde ayırmayı çok daha kolay hale getirir.
  • EithersVeri yapılarında depolayın . Bunları gözden geçirebilir ve hata mesajlarını birleştirebilir, başarısız olmayan ilkini bulabilir, tembel bir şekilde değerlendirebilir veya benzer şekilde yapabilirsiniz.
  • Genişletin ve özelleştirin. Yazmak zorunda kaldığınız bazı kazanları sevmiyor musunuz Eithers? Özel kullanım durumlarınız için birlikte çalışmayı kolaylaştırmak için yardımcı işlevler yazabilirsiniz. Try-catch'in aksine, dil tasarımcılarının bulduğu varsayılan arayüzle kalıcı olarak sıkışıp kalmazsınız.

Çoğu kullanım durumunda, a Trydaha basit bir arayüze sahiptir, yukarıdaki avantajların çoğuna sahiptir ve genellikle ihtiyaçlarınızı da karşılar. Önemli olan Optiontek şey başarı ya da başarısızlıksa ve hata durumu hakkında hata mesajları gibi herhangi bir durum bilgisine ihtiyaç duymuyorsanız, daha da basittir.

Benim deneyimime göre , belki de bir doğrulama hatası mesajı dışında bir şey olmadıkça ve değerleri toplamak istemiyorsanız Eithersgerçekten tercih edilmez . Örneğin, bazı girdileri işliyorsunuz ve yalnızca ilk girişte hata değil tüm hata mesajlarını toplamak istiyorsunuz . Bu durumlar en azından benim için pratikte çok sık ortaya çıkmıyor.TrysLeftExceptionLeft

Try-catch modelinin ötesine bakın ve çalışmak için Eithersçok daha hoş bulacaksınız .

Güncelleme: Bu soru hala dikkat çekiyor ve OP'nin sözdizimi sorusunu açıklayan güncellemesini görmedim, bu yüzden daha fazlasını ekleyeceğimi düşündüm.

İstisna zihniyetten kopma örneği olarak, bu tipik olarak örnek kodunuzu yazmak istiyorum:

def compute(): Either[String, Int] = {
  Right(someSeq)
    .filterOrElse(someACondition,            "someACondition failed")
    .filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
    .map(_ map {_.toInt})
    .filterOrElse(_.sum <= 22, "Sum is greater than 22")
    .map(compute("good",_))
}

Burada filterOrElse, öncelikle bu işlevde yaptığımız şeyi mükemmel bir şekilde semantiklerin olduğunu görebilirsiniz : koşulları kontrol etmek ve hata mesajlarını verilen hatalarla ilişkilendirmek. Bu çok yaygın bir kullanım örneği olduğundan, filterOrElsestandart kitaplıktadır, ancak değilse, oluşturabilirsiniz.

Bu, birisinin benimkiyle tam olarak aynı şekilde çözülemeyen bir karşı örnekle geldiği nokta, ancak yapmaya çalıştığım nokta Eithers, istisnaların aksine, tek bir kullanım yolu olmaması . Kullanılabilen ve denemeye açık olan tüm işlevleri araştırırsanız, çoğu hata işleme durumu için kodu basitleştirmenin yolları vardır Eithers.


1. tryve ' yi ayırabilmek catchadil bir argümandır, ancak bu kolayca gerçekleştirilemez Trymi? 2. Mekanizmanın Eitheryanında veri yapılarında depolama yapılabilir throws; taşıma hatalarının üstünde ekstra bir katman değil mi? 3. Kesinlikle Exceptions genişletebilirsiniz . Tabii, try-catchyapıyı değiştiremezsiniz , ancak başarısızlıklarla uğraşmak o kadar yaygın ki, dilin bana bunun için bir kazan plakası vermesini istiyorum (ki bunu kullanmak zorunda değilim). Bu, anlama için kötü olduğunu söylemek gibidir, çünkü monadik operasyonlar için bir kaynaktır.
Eyal Roth

Az önce Eithers'nin genişletilebileceğini söylediğinizi fark ettim, açıkça anlaşılamazlar ( sealed traither ikisi de sadece iki uygulama ile bir olmak final). Bunu biraz açıklayabilir misin? Sanırım uzamanın gerçek anlamını kastetmediniz.
Eyal Roth

1
Programlama anahtar kelimesini değil, İngilizce kelimedeki gibi genişlemeyi kastetmiştim. Sık kullanılan kalıpları işlemek için işlevler oluşturarak işlevsellik ekleme.
Karl Bielefeldt

8

İkisi aslında çok farklı. Eithersemantiği, potansiyel olarak başarısız hesaplamaları temsil etmekten çok daha geneldir.

Eitheriki farklı türden birini döndürmenizi sağlar. Bu kadar.

Şimdi, bu iki türden biri bir hatayı temsil edebilir veya göstermeyebilir ve diğeri başarıyı temsil edebilir veya etmeyebilir, ancak bu, birçoğunun sadece bir olası kullanım durumudur. Eitherbundan çok, çok daha geneldir. Bir ad veya tarih dönüşü girmesine izin verilen kullanıcıdan girdiyi okuyan bir yönteminiz de olabilir Either[String, Date].

Veya, C♯'daki lambda değişmezlerini düşünün: ya bir Funcya da bir olarak Expressiondeğerlendirirler, yani ya anonim bir işleve ya da bir AST'ye göre değerlendirirler. C♯'de bu "sihirli bir şekilde" olur, ancak buna uygun bir tür vermek istersen, olurdu Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>. Ancak, bu iki seçenekten hiçbiri hata değildir ve bu ikisinin hiçbiri "normal" veya "istisnai" değildir. Bunlar, aynı işlevden (veya bu durumda, aynı değişmez değere atfedilen) döndürülebilecek iki farklı türdür. [Not: aslında, Funcbaşka bir iki duruma bölünmek zorundadır, çünkü C♯'nin bir Unittürü yoktur ve bu nedenle hiçbir şey döndürmeyen bir şey, bir şeyi döndüren bir şeyle aynı şekilde temsil edilemez,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

Şimdi, Either olabilir bir başarı tipi ve başarısızlık tipini temsil etmek için kullanılabilir. Ve iki türün tutarlı bir düzenini kabul ediyorsanız, iki türden birini tercih etmek için önyargı bile yapabilirsiniz Either, böylece monadik zincirleme yoluyla başarısızlıkları yaymayı mümkün kılabilirsiniz. Ancak bu durumda, Eitherkendi başına sahip olması gerekmediği üzerine belirli bir semantik veriyorsunuz.

Bir Eitherhata türü olarak sabitlenmiş iki türünden birine sahip olan ve hataları yaymak için bir tarafa önyargılı olan, genellikle Errormonad olarak adlandırılır ve potansiyel olarak başarısız bir hesaplamayı temsil etmek için daha iyi bir seçim olacaktır. Scala'da bu tür bir tip denir Try.

O ile istisna kullanımını karşılaştırmak için çok daha mantıklı Tryolan daha Either.

Bu nedenle, Eitherkontrol edilen istisnalar üzerinde neden # 1 neden kullanılabilir: istisnalardan Eitherçok daha geneldir. İstisnaların uygulanmadığı durumlarda bile kullanılabilir.

Bununla birlikte, büyük olasılıkla istisnalar yerine yalnızca kullandığınız Either(veya daha olası olan Try) kısıtlı durumu soruyordunuz . Bu durumda, hala bazı avantajlar veya oldukça farklı yaklaşımlar vardır.

Ana avantajı, hatayı işlemeyi ertelemenizdir. Geri dönüş değerini, bir başarı veya hata olup olmadığına bakmadan saklarsınız. (Sonra ele alın.) Ya da başka bir yere verin. (Birisi bunun için endişelensin.) Onları bir listede topla. (Hepsini bir kerede halledin.) Monadik zincirlemeyi bir işleme hattı boyunca yaymak için kullanabilirsiniz.

İstisnaların anlambilimi dile bağlıdır. Scala'da, örneğin, istisnalar yığını çözer. Bir istisna işleyicisine patlamasına izin verirseniz, işleyici gerçekleştiği yere geri dönemez ve düzeltemez. OTOH, çözmeden önce yığını aşağı doğru yakalar ve düzeltmek için gerekli içeriği yok ederseniz, nasıl devam edeceğiniz konusunda bilinçli bir karar vermek için çok düşük bir bileşende olabilirsiniz.

Bu, örneğin, Smalltalk farklıdır, istisnalar yığını gevşeyin ve devam ettirilebilir, ve sen, (muhtemelen debugger gibi yüksek up gibi) yığınında istisna en yüksek yakalamak waaaaayyyyy hatayı düzeltebilecek yok nereye aşağı in oradan yürütme yığın ve devam. Veya CommonLisp koşulları, istisnalarda olduğu gibi, yükseltme, yakalama ve elleçleme yerine iki (yükseltme ve yakalama-taşıma) yerine üç farklı şeydir.

İstisna yerine gerçek bir hata döndürme türü kullanmak, dili değiştirmek zorunda kalmadan benzer şeyler ve daha fazlasını yapmanızı sağlar.

Ve sonra odada bariz bir fil var: istisnalar bir yan etkidir. Yan etkiler kötüdür. Not: Bunun mutlaka doğru olduğunu söylemiyorum. Ancak bu, bazı insanların sahip olduğu bir bakış açısıdır ve bu durumda bir hata türünün istisnalara göre avantajı açıktır. Dolayısıyla, işlevsel programlama yapmak istiyorsanız, hata tiplerini sadece işlevsel yaklaşım olmaları için kullanabilirsiniz. (Haskell'in istisnaları olduğunu unutmayın. Ayrıca kimsenin bunları kullanmayı sevmediğini de unutmayın.)


Cevabı seviyorum, yine de neden vazgeçmem gerektiğini göremiyorum Exception. Eitherharika bir araç gibi görünüyor, ancak evet, özellikle başarısızlıkları ele almayı istiyorum ve görevim için tamamen güçlü bir araçtan çok daha spesifik bir araç kullanmayı tercih ederim. Hata işlemeyi ertelemek adil bir argümandır, ancak şu anda ele almak istemiyorsa, Trybir yöntem fırlatma yöntemini kullanabilir Exception. Yığın izi çözme işlemi de Either/ etkilerini etkilemez Trymi? Hatalı yayılırken aynı hataları tekrarlayabilirsiniz; Her iki durumda da bir hatanın ne zaman ele alınacağını bilmek önemsiz değildir.
Eyal Roth

Ve "tamamen işlevsel" mantıkla gerçekten tartışamam, değil mi?
Eyal Roth

0

İstisnalar hakkında güzel olan şey, kaynak kodun sizin için istisnai durumu zaten sınıflandırmış olmasıdır. A IllegalStateExceptionve a arasındaki BadParameterExceptionfark, daha güçlü yazılan dillerde farklılaştırmak için çok daha kolaydır. "İyi" ve "kötü" ayrıştırmaya çalışırsanız, emrinizdeki son derece güçlü araçtan, yani Scala derleyicisinden faydalanmıyorsunuzdur. Kendi uzantılarınız Throwablemetin mesajı dizesine ek olarak ihtiyacınız olduğu kadar bilgi içerebilir.

Bu, hayatı kolaylaştırmak için sol öğelerin sarıcısı olarak işlev gören TryScala ortamının gerçek faydasıdır Throwable.


2
Ne demek istediğini anlamıyorum.
Eyal Roth

Eithergerçek bir monad (?) olmadığı için stil sorunları var; leftİstisnai koşullar hakkındaki bilgileri uygun bir yapıya düzgün bir şekilde eklediğinizde , değerin keyfi olması, daha yüksek katma değerden uzaklaşır. Bu nedenle Either, bir durumda dönen taraftaki dizelerin kullanımının karşılaştırılması, try/catchdiğer durumda düzgün yazılanların kullanılması uygun Throwables değildir. Çünkü değerinin Throwablesbilgiyi sağlamak için, ve özlü bir şekilde Trykolları Throwables, Tryya daha iyi bir bahistir ... Eitheryatry/catch
BobDalgleish

Aslında try-catch. Soruda belirtildiği gibi , ikincisi bana çok daha temiz görünüyor ve hatayı işleyen kod (her ikisinde de desen eşleştirme veya try-catch) oldukça açık.
Eyal Roth
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.