초과 (체크 된) 예외를 사용하는 이유는 무엇입니까?


17

얼마 전 나는 자바 대신 스칼라를 사용하기 시작했다. 나를위한 언어들 사이의 "변환"과정의 일부는 Either(확인 된) 대신 s 를 사용하는 방법을 배우고있었습니다 Exception. 나는이 방법을 잠시 동안 코딩 해 왔지만 최근에 그것이 정말로 더 좋은 방법인지 궁금해하기 시작했습니다.

하나의 큰 장점은 Either이상이 Exception더 나은 성능이다; 는 Exception스택 트레이스를 구축 할 필요가 던져되고있다. 내가 아는 한, 던지기 Exception는 까다로운 부분이 아니라 스택 추적을 만드는 것입니다.

그러나, 하나는 항상 / 상속 구성 할 수 있습니다 Exception와 함께 사용 scala.util.control.NoStackTrace, 그리고 왼쪽 어디 더욱 그렇고, 나는 경우를 많이 볼 수 Either사실에 Exception(성능 향상을 삼가고).

또 다른 장점 Either은 컴파일러 안전성입니다. Scala 컴파일러는 처리되지 않은 것에 대해 불평하지 않습니다 Exception(Java 컴파일러와 달리). 그러나 내가 실수하지 않으면이 결정은이 주제에서 논의되는 것과 동일한 추론으로 추론됩니다.

구문 측면에서 보면 Exception스타일이 더 명확 해 보입니다 . 다음 코드 블록을 검사하십시오 (둘 다 동일한 기능을 수행함).

Either 스타일:

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 스타일:

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

후자는 나에게 훨씬 깨끗해 보이며 실패를 처리하는 코드 (패턴 일치 on Either또는 try-catch)는 두 경우 모두 매우 분명합니다.

그래서 내 질문은-왜 Either이상 사용 (확인) Exception입니까?

최신 정보

답을 읽은 후, 나는 딜레마의 핵심을 제시하지 못했을 수도 있음을 깨달았습니다. 내 걱정은 하지 의 부족과 try-catch; 하나는 중 "캐치"는 수 ExceptionTry, 또는를 사용 catch에 예외를 포장 Left.

Either/의 주요 문제 Try는 여러 지점에서 실패 할 수있는 코드를 작성할 때 발생합니다. 이러한 시나리오에서 오류가 발생하면 전체 코드에서 해당 오류를 전파해야하므로 앞에서 언급 한 예에 표시된 것처럼 코드가 더 번거로워집니다.

실제로는 스칼라에서 또 다른 "금기"를 Exception사용하여 s 없이 코드를 깨는 또 다른 방법이 return있습니다. 코드는 여전히 Either접근 방식 보다 명확 하고 Exception스타일 보다 약간 덜 깨끗하지만 잡히지 않을 염려가 없습니다 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))
  }
}

기본적으로 return Leftreplaces throw new Exception및의 암시 적 방법 rightOrReturn은 스택에 자동 예외 전파를 보완하는 것입니다.


이것을 읽으십시오 : mauricio.github.io/2014/02/17/…
Robert Harvey

@RobertHarvey 대부분의 기사에서 다루는 것처럼 보입니다 Try. Eithervs 에 관한 부분은 그 방법의 다른 경우가 "예외적이지 않은"경우에 s가 사용되어야한다고 Exception명시하고 Either있다. 첫째, 이것은 매우 모호한 정의 imho입니다. 둘째, 구문 위약금의 가치가 있습니까? 내 말은, Either구문 오버 헤드가 아니라면 s를 사용하는 것이 실제로 마음에 들지 않을 것 입니다.
Eyal Roth

Either나에게 모나드처럼 보인다. 모나드가 제공하는 기능적 구성 혜택이 필요할 때 사용하십시오. 또는 아닐 수도 있습니다.
Robert Harvey

2
@RobertHarvey : 기술적으로 말하면 Either그 자체로는 모나드가 아닙니다. 왼쪽 또는 오른쪽으로 의 투영 은 모나드이지만 Either그 자체로는 그렇지 않습니다. 그래도 왼쪽이나 오른쪽에 "바이어 싱"하여 모나드로 만들 수 있습니다 . 그러나의 양쪽에 특정 의미를 부여합니다 Either. 스칼라 Either는 원래 편견이 없었지만 최근에 편향되어 있었기 때문에 오늘날에는 실제로 모나드이지만 "모나드 니스"는 고유 속성이 Either아니라 편향의 결과입니다.
Jörg W Mittag

@ JörgWMittag : 글쎄요. 즉, 모든 모나 딕의 장점을 위해 사용할 수 있으며 결과 코드가 얼마나 "깨끗한"지에 특별히 신경 쓰지 않아도됩니다 (기능 스타일 연속 코드가 실제로 예외 버전보다 깨끗하다고 ​​생각합니다).
Robert Harvey

답변:


13

Either명령형 try-catch블록 과 똑같이 만 사용한다면 물론 동일한 구문을 수행하는 다른 구문처럼 보일 것입니다. Eithers각각 A . 그들은 같은 제한이 없습니다. 당신은 할 수 있습니다 :

  • 시도 블록을 작고 연속적으로 유지하는 습관을 멈추십시오. "catch"부분은 Eithers"try"부분 바로 옆에있을 필요는 없습니다. 공통 기능으로 공유 될 수도 있습니다. 이를 통해 우려를 올바르게 분리하는 것이 훨씬 쉬워집니다.
  • Eithers데이터 구조에 저장하십시오 . 당신은 그것들을 반복하고 오류 메시지를 통합하고, 실패하지 않은 첫 번째 메시지를 찾거나, 게으르게 평가하거나, 유사한 것을 할 수 있습니다.
  • 확장하고 사용자 정의하십시오. 당신이 쓸 수있는 상용구를 좋아하지 Eithers않습니까? 헬퍼 함수를 ​​작성하여 특정 유스 케이스에 대한 작업을보다 쉽게 ​​수행 할 수 있습니다. try-catch와 달리 언어 디자이너가 만든 기본 인터페이스 만 영구적으로 고수하지는 않습니다.

대부분의 사용 사례에서 Try인터페이스가 더 단순하고 위의 장점을 대부분 가지고 있으며 일반적으로 요구 사항도 충족합니다. 은 Option당신이 걱정 모두에 대한 성공 또는 실패와 실패 사례에 대한 오류 메시지 등의 모든 상태 정보가 필요하지 않은 경우에도 간단하다.

내 경험상, Eithers가 아마도 유효성 검사 오류 메시지 이외의 것이 아니며 값 을 집계하려는 Trys경우 Left가 아니면 실제로 선호되지 않습니다 . 예를 들어, 일부 입력을 처리 중이며 첫 번째 오류뿐만 아니라 모든 오류 메시지 를 수집하려고합니다 . 그러한 상황은 적어도 나에게 실제로는 자주 나타나지 않습니다.ExceptionLeft

try-catch 모델을 넘어 서면 Eithers훨씬 더 즐겁게 작업 할 수 있습니다.

업데이트 :이 질문은 여전히 ​​주목을 받고 있으며 구문 질문을 명확하게하는 OP의 업데이트를 보지 못했기 때문에 더 추가 할 것이라고 생각했습니다.

예외 사고 방식을 벗어나는 예로, 다음은 일반적으로 예제 코드를 작성하는 방법입니다.

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

여기에서 filterOrElse우리가이 기능에서 주로하고있는 일 을 완벽하게 포착 하는 의미론 : 조건 확인 및 오류 메시지와 지정된 실패의 연관을 볼 수 있습니다. 이것은 매우 일반적인 유스 케이스이므로 filterOrElse표준 라이브러리에 있지만 그렇지 않은 경우 작성할 수 있습니다.

이것은 누군가가 내 것과 똑같은 방식으로 해결할 수없는 반례를 제시하는 지점이지만, 내가하려는 것은 Eithers예외와 달리 사용하는 한 가지 방법이 없다는 것입니다 . 사용할 수 있고 실험이 가능한 모든 기능 을 탐색 하는 경우 대부분의 오류 처리 상황에 맞게 코드를 단순화하는 방법이 있습니다 Eithers.


1. try과 를 분리 할 수 ​​있다는 것은 catch공정한 논쟁이지만,이를 통해 쉽게 달성 할 수는 Try없습니까? 2. Either데이터 구조에 저장 하는 것은 throws메커니즘 과 함께 수행 될 수있다 . 처리 실패 외에 단순히 추가 계층이 아닙니까? 3. 당신은 확실히 Exceptions를 확장 할 수 있습니다 . 물론, try-catch구조를 바꿀 수는 없지만 실패 처리는 너무 일반적이므로 언어가 그에 대한 상용구를 제공하기를 원합니다. 그것은 이해를위한 것이 모나 딕 연산을위한 상용구이기 때문에 이해가 나쁘다는 것을 말하는 것과 같습니다.
Eyal Roth

방금 Eithers를 확장 할 수 있다고 말했음을 분명히 깨달았습니다 ( sealed trait두 가지 구현만으로 final). 그것에 대해 자세히 설명해 주시겠습니까? 나는 당신이 확장의 문자 적 ​​의미를 의미하지 않았다고 가정합니다.
Eyal Roth

1
프로그래밍 키워드가 아닌 영어 단어에서와 같이 확장되었습니다. 공통 패턴을 처리하는 함수를 작성하여 기능 추가
Karl Bielefeldt

8

둘은 실제로 매우 다릅니다. Either의 의미는 잠재적으로 실패한 계산을 나타내는 것보다 훨씬 일반적입니다.

Either서로 다른 두 가지 유형 중 하나를 반환 할 수 있습니다. 그게 다야.

자,이 두 유형 중 하나는 또는 오류 및 다른 표현하지 않을 수 있습니다 5 월 또는 성공을 표현하지 않을 수 있습니다,하지만 많은 단지 하나의 가능한 사용 사례입니다. Either그보다 훨씬 더 일반적입니다. 이름을 입력하거나 날짜를 반환 할 수있는 사용자의 입력을 읽는 방법을 사용할 수도 있습니다 Either[String, Date].

또는 C♯의 람다 리터럴에 대해 생각 Func하십시오 Expression. 즉, a 또는 a로 평가됩니다. 즉, 익명 함수로 평가하거나 AST로 평가합니다. C♯에서, 이것은 "마 법적으로"발생하지만, 적절한 타입을 주길 원한다면 그렇습니다 Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>. 그러나 두 옵션 중 어느 것도 오류가 아니며 두 옵션 중 어느 것도 "정상"또는 "예외"가 아닙니다. 그것들은 동일한 함수에서 리턴 될 수있는 두 개의 다른 타입 일뿐입니다 (이 경우에는 동일한 리터럴을 나타냄). [참고 : FuncC♯에는 Unit유형 이 없기 때문에 실제로 두 가지 경우로 나눠야합니다. 따라서 아무것도 반환하지 않는 것은 무언가를 반환하는 것과 같은 방식으로 표현할 수 없습니다.Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

이제 성공 유형과 실패 유형을 나타내는 데 사용할 Either 있습니다. 그리고 두 가지 유형의 일관된 순서에 동의하는 경우 두 가지 유형 중 하나를 선호하도록 편향 시킬 수 Either있으므로 모나드 체인을 통해 실패를 전파 할 수 있습니다. 그러나이 경우 Either에는 반드시 자체적으로 소유 할 필요가없는 특정 의미를 부여합니다 .

Either오류 유형으로 고정 된 두 개의 타입 중 하나를 가지며, 에러를 전파 한쪽으로 편향된다 즉, 보통 호출 Error모나드, 그리고 잠재적으로 실패 연산을 표현하기 위해 더 나은 선택 일 것이다. 스칼라에서는 이런 유형을이라고 Try합니다.

예외 사용과와 비교하는 것이 훨씬 더 합리적 Try입니다 Either.

이것이 이유 # 1 인 이유 Either 검사 된 예외에 사용될 수있는Either 입니다. 예외보다 훨씬 일반적입니다. 예외가 적용되지 않는 경우에도 사용할 수 있습니다.

그러나 방금 사용한 Either(또는 더 가능성이 높은 제한된 경우에 대해)Try 예외의 대체물로 ) . 이 경우 여전히 몇 가지 장점이 있거나 다른 접근법이 가능합니다.

주요 이점은 오류 처리를 연기 할 수 있다는 것입니다. 성공 여부를 보지 않고도 반환 값을 저장하기 만하면됩니다. (나중에 처리하십시오.) 또는 다른 곳으로 전달하십시오. (다른 사람이 걱정하게하십시오.) 목록에서 수집하십시오. (모두 한 번에 처리하십시오.) 모나 딕 체인을 사용하여 처리 파이프 라인을 따라 전파 할 수 있습니다.

예외의 의미는 언어에 고정되어 있습니다. 예를 들어 스칼라에서는 예외가 스택을 해제합니다. 예외 처리기로 버블 링하면 처리기가 발생한 위치로 되돌아 가서 수정할 수 없습니다. OTOH, 스택을 풀고 그것을 수정하기 위해 필요한 컨텍스트를 파괴하기 전에 스택에서 더 낮게 잡으면 너무 낮은 수준의 구성 요소에있을 수 있으므로 진행 방법에 대한 정보를 결정할 수 있습니다.

예를 들어, 예외가 스택을 풀지 않고 다시 시작할 수있는 스몰 토크에서는 다릅니다. 스택에서 예외를 높게 잡아낼 수 있고 (디버거만큼 높을 수 있음) 오류를 수정합니다. waaaaayyyyy down 에서 거기에서 실행을 스택하고 다시 시작하십시오. 또는 예외와 같이 양육, 잡기 및 취급이 두 가지 (상승 및 잡기 처리) 대신 세 가지로 다른 CommonLisp 조건.

예외 대신 실제 오류 반환 유형을 사용하면 언어를 수정하지 않고도 유사한 작업을 수행 할 수 있습니다.

그리고 그 방에 명백한 코끼리가 있습니다 : 예외는 부작용입니다. 부작용은 악하다. 참고 : 이것이 반드시 사실이라고 말하는 것은 아닙니다. 그러나 일부 사람들의 견해이며,이 경우 예외에 대한 오류 유형의 장점이 분명합니다. 따라서 함수형 프로그래밍을 수행하려는 경우 함수형 접근 방식이라는 이유만으로 오류 유형을 사용할 수 있습니다. (하스켈에는 예외가 있습니다. 또한 아무도 사용하는 것을 좋아하지 않습니다.)


나는 왜 아직도 잊어야하는지 모르겠지만 대답을 좋아하십시오 Exception. Either훌륭한 도구처럼 보이지만, 그렇습니다. 실패를 처리하는 것에 대해 구체적으로 요구하고 있으며, 강력한 작업보다 내 작업에 훨씬 더 구체적인 도구를 사용하고 싶습니다. 실패 처리 지연은 공정한 논거이지만 , 현재 처리하지 않으려 TryException경우에는 메소드를 던질 수 있습니다 . 스택 추적 해제는 Either/ Try에도 영향을 미치지 않습니까? 잘못 전파 할 때도 같은 실수를 반복 할 수 있습니다. 실패를 처리 할시기를 아는 것이 두 경우 모두 사소한 것은 아닙니다.
Eyal Roth

그리고 나는 "순전히 기능적인"추론으로 논쟁 할 수 없습니다.
Eyal Roth

0

예외에 대한 좋은 점은 원래 코드가 이미 예외적 인 상황을 분류했다는 것입니다. 의 차이 IllegalStateException와는 BadParameterException강한 입력 된 언어로 차별화하기가 훨씬 쉽다. "좋음"과 "나쁨"을 구문 분석하려고하면 스칼라 컴파일러와 같은 강력한 도구를 활용할 수 없습니다. 에 대한 자신의 확장Throwable텍스트 메시지 문자열 외에도 필요한만큼의 정보를 포함 수있는 입니다.

이것은 TryScala 환경에서 진정한 유틸리티이며 , Throwable삶을 더 쉽게 만들기 위해 왼쪽 항목의 래퍼 역할을합니다.


2
나는 당신의 요점을 이해하지 못합니다.
Eyal Roth

Either진정한 모나드 (?)가 아니기 때문에 스타일 문제가 있습니다. left가치 의 임의성은 예외적 인 조건에 대한 정보를 적절한 구조로 적절히 묶을 때 더 높은 가치에서 멀어지게합니다. 따라서 Either한 경우에는 왼쪽에서 문자열을 반환 try/catch하는 것과 다른 경우에는 올바르게 입력하는 것을 사용하는 것이 적절 Throwables 하지 않습니다. 때문에 값의 Throwables정보를 제공하고있는 간결한 방식으로 Try핸들 Throwables, Try하나보다 더 나은 내기가 ... Eithertry/catch
BobDalgleish

나는 실제로 걱정하지 않는다 try-catch. 질문 에서
Eyal Roth
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.