Perché usare Eccezione over (selezionata)?


17

Non molto tempo fa ho iniziato a utilizzare Scala anziché Java. Per me, parte del processo di "conversione" tra le lingue stava imparando a usare Eithers anziché (controllato) Exceptions. Ho programmato in questo modo per un po ', ma recentemente ho iniziato a chiedermi se è davvero un modo migliore per andare.

Uno dei maggiori vantaggi Eitherè Exceptionrappresentato dalle prestazioni migliori; è Exceptionnecessario creare una traccia stack e viene lanciata. Per quanto ne capisco, però, lanciare la parte Exceptionnon è impegnativo, ma costruire lo stack-trace è.

Ma poi, si può sempre costruire / ereditare Exceptions scala.util.control.NoStackTrace, e ancora di più, vedo molti casi in cui il lato sinistro di un Eitherè in realtà un Exception(rinunciare all'aumento delle prestazioni).

Un altro vantaggio Eitherè la sicurezza del compilatore; il compilatore Scala non si lamenterà di Exceptions non gestiti (diversamente dal compilatore Java). Ma se non sbaglio, questa decisione è motivata con lo stesso ragionamento che viene discusso in questo argomento, quindi ...

In termini di sintassi, mi sembra che lo Exceptionstile sia molto più chiaro. Esaminare i seguenti blocchi di codice (entrambi raggiungendo la stessa funzionalità):

Either stile:

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

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

Quest'ultimo mi sembra molto più pulito, e il codice che gestisce l'errore (o pattern-matching su Eithero try-catch) è abbastanza chiaro in entrambi i casi.

Quindi la mia domanda è: perché usare Eitheroltre (selezionato) Exception?

Aggiornare

Dopo aver letto le risposte, mi sono reso conto che avrei potuto non riuscire a presentare il nucleo del mio dilemma. La mia preoccupazione non riguarda la mancanza di try-catch; si può "catturare" un Exceptioncon Tryo usare il catchper avvolgere l'eccezione con Left.

Il mio problema principale con Either/ Tryviene quando scrivo codice che potrebbe non riuscire in molti punti lungo la strada; in questi scenari, quando si verifica un errore, devo propagare quell'errore in tutto il mio codice, rendendo il codice molto più ingombrante (come mostrato negli esempi di cui sopra).

Esiste in realtà un altro modo per violare il codice senza Exceptionusare return(che in realtà è un altro "tabù" in Scala). Il codice sarebbe ancora più chiaro diEither dell'approccio e, pur essendo un po 'meno pulito dello Exceptionstile, non ci sarebbe paura di non essere catturati 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))
  }
}

Fondamentalmente i return Leftrimpiazzi throw new Exceptione il metodo implicito su entrambi,rightOrReturn , è un supplemento per la propagazione automatica delle eccezioni nello stack.



@RobertHarvey Sembra che la maggior parte dell'articolo discuta Try. La parte su Eithervs Exceptionafferma semplicemente che Eithers dovrebbe essere usato quando l'altro caso del metodo è "non eccezionale". Innanzitutto, questa è una definizione molto, molto vaga. Secondo, vale davvero la pena della sintassi? Voglio dire, non mi dispiacerebbe davvero usare Eithers se non fosse per la sintassi ambientale che presentano.
Eyal Roth,

Eithermi sembra una monade. Usalo quando hai bisogno dei vantaggi della composizione funzionale che offrono le monadi. O forse no .
Robert Harvey,

2
@RobertHarvey: tecnicamente parlando, di Eitherper sé non è una monade. La proiezione sul lato sinistro o destro è una monade, ma di Eitherper sé non lo è. Puoi renderla una monade, "tendendola" al lato sinistro o destro, però. Tuttavia, impartisci un certo semantico su entrambi i lati di un Either. La Scala Eitherera originariamente imparziale, ma era di parte piuttosto di recente, quindi oggigiorno è in realtà una monade, ma la "monadness" non è una proprietà intrinseca di, Eitherma piuttosto il risultato di essere di parte.
Jörg W Mittag,

@ JörgWMittag: bene. Ciò significa che puoi usarlo per tutta la sua bontà monadica e non essere particolarmente infastidito da quanto sia "pulito" il codice risultante (anche se sospetto che il codice di continuazione in stile funzionale sarebbe effettivamente più pulito della versione Eccezione).
Robert Harvey,

Risposte:


13

Se usi solo un blocco Eitherimperativo esattamente simile try-catch, ovviamente sembrerà una sintassi diversa per fare esattamente la stessa cosa. Eitherssono un valore . Non hanno gli stessi limiti. Puoi:

  • Ferma l'abitudine di mantenere i blocchi di prova piccoli e contigui. La parte "catch" di Eithersnon ha bisogno di essere proprio accanto alla parte "try". Potrebbe anche essere condiviso in una funzione comune. Ciò rende molto più semplice separare correttamente le preoccupazioni.
  • Archivia Eithersin strutture dati. È possibile eseguire il ciclo tra loro e consolidare i messaggi di errore, trovare il primo che non ha fallito, valutarli pigramente o simili.
  • Ampliali e personalizzali. Non ti piace un po 'di piastra con cui sei costretto a scrivere Eithers? È possibile scrivere funzioni di supporto per semplificare la gestione di casi d'uso particolari. A differenza di try-catch, non sei permanentemente bloccato solo con l'interfaccia predefinita creata dai progettisti del linguaggio.

Nota per la maggior parte dei casi d'uso, a Tryha un'interfaccia più semplice, ha la maggior parte dei vantaggi di cui sopra e di solito soddisfa anche le tue esigenze. An Optionè ancora più semplice se tutto ciò che ti interessa è il successo o il fallimento e non hai bisogno di informazioni sullo stato come i messaggi di errore sul caso di fallimento.

Nella mia esperienza, Eithersnon sono davvero preferito a Trysmeno che non Leftsia qualcosa di diverso da un Exception, forse un messaggio di errore di convalida, e si desidera aggregare i Leftvalori. Ad esempio, stai elaborando alcuni input e vuoi raccogliere tutti i messaggi di errore, non solo l'errore sul primo. Quelle situazioni non si presentano molto spesso in pratica, almeno per me.

Guarda oltre il modello try-catch e troverai Eithersmolto più piacevole lavorare con te.

Aggiornamento: questa domanda sta ancora ottenendo attenzione e non avevo visto l'aggiornamento dell'OP che chiariva la domanda di sintassi, quindi ho pensato di aggiungere altro.

Come esempio per uscire dalla mentalità dell'eccezione, questo è il modo in cui scriverei normalmente il tuo codice di esempio:

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",_))
}

Qui puoi vedere che la semantica di filterOrElsecatturare perfettamente ciò che stiamo facendo principalmente in questa funzione: controllare le condizioni e associare i messaggi di errore ai guasti indicati. Poiché questo è un caso d'uso molto comune,filterOrElse è nella libreria standard, ma se non lo fosse, è possibile crearlo.

Questo è il punto in cui qualcuno trova un contro-esempio che non può essere risolto esattamente come il mio, ma il punto che sto cercando di fare è che non c'è un solo modo di usare Eithers, a differenza delle eccezioni. Esistono modi per semplificare il codice per la maggior parte delle situazioni di gestione degli errori, se si esplorano tutte le funzioni disponibili per l'uso Eitherse si è aperti alla sperimentazione.


1. Essere in grado di separare trye catchè un argomento equo, ma non è possibile farlo facilmente Try? 2. L'archiviazione di messaggi di testo Eitherin strutture di dati potrebbe essere eseguita insieme al throwsmeccanismo; non è semplicemente un ulteriore livello in aggiunta alla gestione degli errori? 3. Puoi sicuramente estendere Exceptions. Certo, non puoi cambiare la try-catchstruttura, ma la gestione degli errori è così comune che voglio assolutamente che la lingua mi dia una piastra di controllo (che non sono costretto a usare). È come dire che la comprensione è negativa perché è una piastra di comando per le operazioni monadiche.
Eyal Roth,

Mi sono appena reso conto che hai detto che Eitherè possibile estendere s, dove chiaramente non possono (essendo una sealed traitcon solo due implementazioni, entrambe final). Puoi approfondire questo? Suppongo che non intendessi il significato letterale di estensione.
Eyal Roth,

1
Intendevo esteso come nella parola inglese, non la parola chiave di programmazione. Aggiunta di funzionalità creando funzioni per gestire modelli comuni.
Karl Bielefeldt,

8

I due sono in realtà molto diversi. EitherLa semantica è molto più generale del semplice rappresentare potenziali calcoli.

Eitherconsente di restituire uno di due tipi diversi. Questo è tutto.

Ora, uno di questi due tipi può o meno rappresentare un errore e l'altro può o meno rappresentare il successo, ma questo è solo un possibile caso d'uso di molti. Eitherè molto, molto più generale di così. Potresti anche avere un metodo che legge l'input dell'utente a cui è consentito inserire un nome o una data di ritorno a Either[String, Date].

Oppure, pensa ai valori letterali lambda in C♯: valutano su an Funco an Expression, ovvero valutano su una funzione anonima o valutano su un AST. In C♯, questo accade "magicamente", ma se si volesse dargli un tipo appropriato, lo sarebbe Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>. Tuttavia, nessuna delle due opzioni è un errore e nessuna delle due è "normale" o "eccezionale". Sono solo due tipi diversi che possono essere restituiti dalla stessa funzione (o in questo caso, attribuiti allo stesso valore letterale). [Nota: in realtà, Funcdeve essere suddiviso in altri due casi, perché C♯ non ha un Unittipo e quindi qualcosa che non restituisce nulla non può essere rappresentato allo stesso modo di qualcosa che restituisce qualcosa,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

Ora, Either può essere utilizzato per rappresentare un tipo di successo e un tipo di errore. E se siete d'accordo su un ordinamento coerente dei due tipi, si può anche pregiudizi Either a preferire uno dei due tipi, rendendo così possibile propagare i guasti attraverso il concatenamento monadica. Ma in quel caso, stai impartendo un certo semantico sul fatto Eitherche non necessariamente possiede da solo.

Uno Eitherche ha uno dei suoi due tipi fissato per essere un tipo di errore ed è distorto da un lato per propagare gli errori, di solito viene chiamato Errormonade e sarebbe una scelta migliore per rappresentare un calcolo potenzialmente in errore. Alla Scala si chiama questo tipo di tipo Try.

Ha molto più senso confrontare l'uso delle eccezioni con Tryche con Either.

Quindi, questo è il motivo n. 1 per cui Eitherpotrebbe essere utilizzato su eccezioni verificate: Eitherè molto più generale delle eccezioni. Può essere utilizzato nei casi in cui non si applicano nemmeno le eccezioni.

Tuttavia, molto probabilmente stavi chiedendo del caso limitato in cui usi Either(o più probabilmente Try) come sostituto delle eccezioni. In tal caso, ci sono ancora alcuni vantaggi, o piuttosto possibili approcci diversi.

Il vantaggio principale è che puoi rimandare la gestione dell'errore. Memorizzi semplicemente il valore restituito, senza nemmeno guardare se si tratta di un successo o di un errore. (Gestiscilo più tardi.) O passalo altrove. (Lascia che qualcun altro si preoccupi.) Raccoglili in un elenco. (Gestirli tutti in una volta.) È possibile utilizzare il concatenamento monadico per propagarli lungo una pipeline di elaborazione.

La semantica delle eccezioni è cablata nella lingua. In Scala, ad esempio, le eccezioni svolgono lo stack. Se li lasci passare a un gestore di eccezioni, il gestore non può tornare indietro dove è successo e risolverlo. OTOH, se lo prendi più in basso nello stack prima di srotolarlo e distruggere il contesto necessario per risolverlo, potresti essere in un componente di livello troppo basso per prendere una decisione informata su come procedere.

Ciò è diverso in Smalltalk, ad esempio, in cui le eccezioni non srotolano lo stack e sono ripristinabili e puoi rilevare l'eccezione in alto nello stack (possibilmente in alto come il debugger), correggere l'errore waaaaayyyyy down nel impilare e riprendere l'esecuzione da lì. O condizioni CommonLisp in cui il sollevamento, la cattura e la gestione sono tre cose diverse anziché due (raccolta e gestione della cattura) come con le eccezioni.

L'utilizzo di un tipo di ritorno dell'errore effettivo anziché di un'eccezione consente di eseguire operazioni simili e altro, senza dover modificare la lingua.

E poi c'è l'ovvio elefante nella stanza: le eccezioni sono un effetto collaterale. Gli effetti collaterali sono cattivi. Nota: non sto dicendo che questo è necessariamente vero. Ma è un punto di vista che alcune persone hanno, e in quel caso, il vantaggio di un tipo di errore rispetto alle eccezioni è evidente. Pertanto, se si desidera eseguire una programmazione funzionale, è possibile utilizzare i tipi di errore per il solo motivo che si tratta dell'approccio funzionale. (Nota che Haskell ha delle eccezioni. Nota anche che a nessuno piace usarle.)


Adoro la risposta, anche se ancora non vedo perché dovrei rinunciare a Exceptions. Eithersembra un ottimo strumento, ma sì, sto specificatamente chiedendo informazioni sulla gestione degli errori e preferirei utilizzare uno strumento molto più specifico per il mio compito rispetto a uno onnipotente. Il differimento della gestione degli errori è una buona argomentazione, ma si può semplicemente usare Tryun metodo che getta un Exceptionse desidera non gestirlo al momento. Anche lo svolgersi dello stack-trace non ha effetto Either/ Try? Puoi ripetere gli stessi errori quando li propaghi in modo errato; sapere quando gestire un errore non è banale in entrambi i casi.
Eyal Roth,

E non posso davvero discutere con ragionamenti "puramente funzionali", vero?
Eyal Roth,

0

La cosa bella delle eccezioni è che il codice di origine ha già classificato le circostanze eccezionali per te. La differenza tra an IllegalStateExceptione a BadParameterExceptionè molto più facile da differenziare in linguaggi tipizzati più forti. Se provi ad analizzare "buono" e "cattivo", non stai sfruttando lo strumento estremamente potente a tua disposizione, vale a dire il compilatore Scala. Le tue estensioni Throwablepossono includere tutte le informazioni di cui hai bisogno, oltre alla stringa di messaggi testuali.

Questa è la vera utilità Trynell'ambiente Scala, con la Throwablefunzione di involucro degli oggetti a sinistra per semplificare la vita.


2
Non capisco il tuo punto.
Eyal Roth,

Eitherha problemi di stile perché non è una vera monade (?); l'arbitrarietà del leftvalore si allontana dal valore aggiunto più elevato quando si racchiudono correttamente informazioni su condizioni eccezionali in una struttura adeguata. Pertanto, confrontando l'uso di Eitherin un caso restituendo stringhe nella parte sinistra, rispetto try/catchnell'altro caso l'uso corretto Throwables non è corretto. A causa del valore di Throwablesfornire informazioni e del modo conciso che Trygestisce Throwables, Tryè una scommessa migliore rispetto a ... Eitherotry/catch
BobDalgleish

In realtà non sono preoccupato per il try-catch. Come detto nella domanda, quest'ultimo mi sembra molto più pulito, e il codice che gestisce l'errore (o pattern-matching su entrambi o try-catch) è abbastanza chiaro in entrambi i casi.
Eyal Roth,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.