기능적 스타일 예외 처리


24

함수형 프로그래밍에서 예외를 던지거나 관찰해서는 안된다는 말을 들었습니다. 대신 잘못된 계산은 최저값으로 평가해야합니다. 파이썬 (또는 함수형 프로그래밍을 완전히 장려하지 않는 다른 언어) 에서는 무언가가 "순수하게 유지"되는 것이 잘못 될 때마다 리턴 할 수 있습니다 None(또는 None정의를 엄격하게 준수하지는 않지만 다른 값을 최저값으로 취급 함 ). 먼저 오류를 관찰해야합니다. 즉

def fn(*args):
    try:
        ... do something
    except SomeException:
        return None

이것이 순결을 위반합니까? 그렇다면 파이썬에서 순수하게 오류를 처리하는 것이 불가능하다는 것을 의미합니까?

최신 정보

그의 의견에서 Eric Lippert는 FP에서 예외를 처리하는 또 다른 방법을 상기시켰다. 실제로 파이썬에서 한 일을 본 적이 없지만 1 년 전에 FP를 공부했을 때 나는 그것을 가지고 놀았습니다. 여기서 optional-decorated 함수는 Optional지정된 예외 목록뿐만 아니라 일반 출력에 대해 비어있을 수있는 값을 리턴 합니다 (지정되지 않은 예외는 여전히 실행을 종료 할 수 있음). Carry각 단계 (지연된 함수 호출)가 Optional이전 단계에서 비어 있지 않은 출력을 가져 와서 단순히 전달하거나 그렇지 않으면 new를 전달하여 평가하는 지연된 평가를 작성합니다 Optional. 결국 최종 값은 normal 또는 Empty입니다. 여기서 try/except블록은 데코레이터 뒤에 숨겨 지므로 지정된 예외는 반환 유형 서명의 일부로 간주 될 수 있습니다.

class Empty:
    def __repr__(self):
        return "Empty"


class Optional:
    def __init__(self, value=Empty):
        self._value = value

    @property
    def value(self):
        return Empty if self.isempty else self._value

    @property
    def isempty(self):
        return isinstance(self._value, BaseException) or self._value is Empty

    def __bool__(self):
        raise TypeError("Optional has no boolean value")


def optional(*exception_types):
    def build_wrapper(func):
        def wrapper(*args, **kwargs):
            try:
                return Optional(func(*args, **kwargs))
            except exception_types as e:
                return Optional(e)
        wrapper.__isoptional__ = True
        return wrapper
    return build_wrapper


class Carry:
    """
    >>> from functools import partial
    >>> @optional(ArithmeticError)
    ... def rdiv(a, b):
    ...     return b // a
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 0) >> partial(rdiv, 1))(1)
    1
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 1))(1)
    1
    >>> (Carry() >> rdiv >> rdiv)(0, 1) is Empty
    True
    """
    def __init__(self, steps=None):
        self._steps = tuple(steps) if steps is not None else ()

    def _add_step(self, step):
        fn, *step_args = step if isinstance(step, Sequence) else (step, )
        return type(self)(steps=self._steps + ((fn, step_args), ))

    def __rshift__(self, step) -> "Carry":
        return self._add_step(step)

    def _evaluate(self, *args) -> Optional:
        def caller(carried: Optional, step):
            fn, step_args = step
            return fn(*(*step_args, *args)) if carried.isempty else carried
        return reduce(caller, self._steps, Optional())

    def __call__(self, *args):
        return self._evaluate(*args).value

1
귀하의 질문에 이미 답변되었으므로 주석 만하십시오 : 함수형 프로그래밍에서 예외를 던지는 것이 찌그러 지는지 이해 합니까? 그것은 임의의 변덕이 아닙니다 :)
Andres F.

6
오류를 나타내는 값을 반환하는 또 다른 대안이 있습니다. 예외 처리는 제어 흐름 이며 제어 흐름 을 구체화하기위한 기능적 메커니즘이 있습니다. 성공 연속오류 연속 이라는 두 가지 기능을 수행하는 메소드를 작성하여 기능적 언어에서 예외 처리를 에뮬레이트 할 수 있습니다 . 함수가 마지막으로 수행하는 것은 "결과"를 전달하는 성공 지속 또는 "예외"를 전달하는 오류 지속을 호출하는 것입니다. 단점은 이러한 내부 방식으로 프로그램을 작성해야한다는 것입니다.
Eric Lippert

3
아니요,이 경우의 상태는 어떻습니까? 여러 가지 문제가 있지만 여기에 몇 가지가 있습니다. 1- 유형으로 인코딩되지 않은 가능한 흐름이 있습니다. 즉, 유형을 살펴보면 함수가 예외를 throw하는지 여부를 알 수 없습니다 (확인하지 않은 경우 제외) 예외는, 물론,하지만 난 것을 언어 모릅니다 )을 가지고 있습니다. 유형 시스템을 "외부"로 효과적으로 작업하고 있습니다. 2 명의 기능을 갖춘 프로그래머는 가능한 한 "전체"기능, 즉 모든 입력에 대해 값을 반환하는 기능 (비 종료 제외)을 작성하려고합니다. 이에 대한 예외는 작동합니다.
Andres F.

3
3- 총 함수를 사용할 때 "함수로 인코딩되지 않은"오류 결과 (예 : 예외)에 대해 걱정하지 않고 다른 함수와 함께 작성하고 고차 함수에서 사용할 수 있습니다.
Andres F.

1
@EliKorvigo 변수를 설정하면 정의에 의해 전체 정지 상태가됩니다. 변수의 전체 목적은 상태를 유지하는 것입니다. 그것은 예외와 관련이 없습니다. 함수의 반환 값을 관찰하고 관찰을 기반으로 변수의 값을 설정하면 상태가 평가에 도입되지 않습니까?
user253751

답변:


20

우선, 오해를 정리해 봅시다. "하단 값"이 없습니다. 맨 아래 유형은 언어에서 다른 모든 유형의 하위 유형 인 유형으로 정의됩니다. 이것으로부터, 적어도 흥미로운 타입 시스템에서, 맨 아래 타입 에는 값이 없다는 것을 증명할 수 있습니다 - 비어 있습니다. 따라서 최저값과 같은 것은 없습니다.

하단 유형이 유용한 이유는 무엇입니까? 비어 있음을 알면 프로그램 동작을 약간 공제하겠습니다. 예를 들어, 함수가 있다면 :

def do_thing(a: int) -> Bottom: ...

우리 do_thing는 type의 값을 반환해야하기 때문에 결코 반환 할 수 없다는 것을 알고 있습니다 Bottom. 따라서 두 가지 가능성 만 있습니다.

  1. do_thing 멈추지 않는다
  2. do_thing 예외를 발생시킵니다 (예외 메커니즘이있는 언어로)

Bottom파이썬 언어에는 실제로 존재하지 않는 유형 을 만들었습니다 . None잘못된 이름입니다. 실제로 단위 값단위 유형 의 유일한 값이며 NoneType파이썬에서 호출 됩니다 ( type(None)자신을 확인하십시오).

이제 또 다른 오해는 기능적 언어에는 예외가 없다는 것입니다. 이것은 사실이 아닙니다. 예를 들어 SML에는 매우 훌륭한 예외 메커니즘이 있습니다. 그러나 예외는 SML에서 파이썬보다 훨씬 적게 사용됩니다. 앞서 말했듯이, 기능적 언어에서 어떤 종류의 실패를 나타내는 일반적인 방법은 Option유형 을 반환하는 것 입니다. 예를 들어 다음과 같이 안전한 나누기 함수를 만듭니다.

def safe_div(num: int, den: int) -> Option[int]:
  return Some(num/den) if den != 0 else None

불행히도 파이썬에는 실제로 합계 유형이 없으므로 실행 가능한 방법이 아닙니다. 실패를 나타 내기 위해 가난한 사람의 옵션 유형으로 돌아올 수 None 있지만 실제로는 return하는 것보다 낫지 않습니다 Null. 형식 안전이 없습니다.

이 경우 언어 규칙을 따르는 것이 좋습니다. 파이썬은 제어 흐름을 처리하기 위해 관용적으로 예외를 사용합니다 (잘못된 디자인, IMO이지만 그럼에도 불구하고 표준 임). 그렇기 때문에 직접 작성한 코드로 작업하지 않는 한 표준 연습을 따르는 것이 좋습니다. 이것이 "순수"인지 여부는 관련이 없습니다.


"bottom value"라는 용어는 실제로 "bottom type"을 의미 None하므로 정의를 따르지 않는 필자가 작성했습니다 . 어쨌든, 수정 해 주셔서 감사합니다. 파이썬의 원칙에 따라 예외를 사용하여 실행을 완전히 중지하거나 선택적 값을 반환하는 것이 좋다고 생각하지 않습니까? 복잡한 제어를 위해 예외를 사용하지 않는 것이 왜 나쁜가요?
Eli Korvigo

@EliKorvigo 그게 내가 뭐라고 말 했는가? 관용적 파이썬은 예외입니다.
gardenhead

1
예를 들어, 저학년 학생들이 try/except/finally다른 대안 으로 사용하지 말 것을 권장 합니다 if/else. 즉 try: var = expession1; except ...: var = expression 2; except ...: var = expression 3..., 명령형 언어로하는 것이 일반적이지만 (btw, 나는 if/else이것에 대해서도 블록을 사용하지 않는 것이 좋습니다 ). 내가 "이것이 파이썬"이기 때문에 불합리하고 그런 패턴을 허용해야한다는 것을 의미합니까?
Eli Korvigo

@EliKorvigo 나는 당신에게 일반적으로 동의합니다 (btw, 당신은 교수입니까?). 제어 흐름에 사용 try... catch...해서는 안됩니다 . 어떤 이유로, 그것은 파이썬 커뮤니티가 일을하기로 결정한 방법입니다. 예를 들어 safe_div위에서 작성한 함수는 일반적으로 작성 try: result = num / div: except ArithmeticError: result = None됩니다. 따라서 일반적인 소프트웨어 엔지니어링 원칙을 가르치는 경우이를 권장하지 않아야합니다. if ... else...코드 냄새이기도하지만 여기에 들어가기가 너무 깁니다.
gardenhead

2
"하단 값"이 있으며 (예를 들어 Haskell의 의미론에 사용되는 데 사용됨) 아래쪽 유형과는 거의 관련이 없습니다. 따라서 그것은 실제로 OP에 대한 오해가 아니며 단지 다른 것에 대해서만 이야기합니다.
Ben

11

지난 며칠 동안 순도에 관심이 많았으므로 순수한 함수가 어떻게 보이는지 살펴 보지 않겠습니까?

순수한 기능 :

  • 참조가 투명합니다. 즉, 주어진 입력에 대해 항상 동일한 출력을 생성합니다.

  • 부작용을 일으키지 않습니다. 외부 환경에서 입력, 출력 또는 다른 것을 변경하지 않습니다. 반환 값만 생성합니다.

스스로에게 물어보십시오. 함수가 입력을 받아들이고 출력을 반환하는 것 외에 다른 작업을 수행합니까?


3
기능적으로 작동하는 한 얼마나 추악하게 쓰여져 있습니까?
Eli Korvigo

8
순도에만 관심이 있다면 맞습니다. 물론 고려해야 할 다른 것들이있을 수 있습니다.
Robert Harvey

3
악마를 옹호하기 위해 예외를 던지는 것은 단지 출력의 형태와 던지는 순수한 기능이라고 주장합니다. 그게 문제입니까?
Bergi

1
@ Bergi 나는 당신이 악마의 옹호자를하고 있다는 것을 모른다. 왜냐하면 이것이 대답이 의미하는 바이기 때문이다. 문제는 순도 외에 다른 고려 사항 이 있다는 것이다. 검사되지 않은 예외 (정의상 함수 서명의 일부가 아님)를 허용하면 모든 함수의 리턴 유형이 효과적으로 T + { Exception }( T명시 적으로 선언 된 리턴 유형이 됨) 문제가됩니다. 소스 코드를 보지 않고 함수가 예외를 throw하는지 여부를 알 수 없으므로 고차 함수 작성도 문제가됩니다.
Andres F.

2
던지는 동안 @Begri은 틀림없이 순수 할 수 있지만 예외를 사용하는 IMO는 각 함수의 반환 유형을 복잡하게 만드는 것 이상을 수행합니다. 기능의 구성 성을 손상시킵니다. 오류가 발생할 수 map : (A->B)->List A ->List B있는 곳 의 구현을 고려하십시오 A->B. f가 예외를 던지도록 허용하면 map f L 유형의 무언가를 반환합니다 Exception + List<B>. 대신 optional스타일 유형 을 반환하도록 허용하면 대신 map f LList <Optional <B >>` 를 반환합니다 . 이 두 번째 옵션은 더 기능적입니다.
Michael Anderson

7

Haskell 시맨틱은 "하단 값"을 사용하여 Haskell 코드의 의미를 분석합니다. Haskell 프로그래밍에 직접 사용하는 None것은 아니며 반환 은 전혀 같은 종류가 아닙니다.

최하위 값은 Haskell 시맨틱이 값으로 정상적으로 평가하지 않는 계산에 부여 된 값입니다. Haskell 계산이 수행 할 수있는 한 가지 방법은 실제로 예외를 발생시키는 것입니다! 따라서 파이썬에서이 스타일을 사용하려고한다면 실제로 평소처럼 예외를 던져야합니다.

하스켈 시맨틱은 하스켈이 게으 르기 때문에 최저값을 사용합니다. 실제로 아직 실행되지 않은 계산에 의해 반환되는 "값"을 조작 할 수 있습니다. 함수에 전달하고 데이터 구조 등에 붙일 수 있습니다. 이러한 평가되지 않은 계산은 예외 또는 루프를 영원히 던질 수 있지만 실제로 값을 조사 할 필요가 없다면 계산은 절대되지 않습니다.실행하고 오류가 발생하면 전체 프로그램이 잘 정의되고 완료된 작업을 수행 할 수 있습니다. 따라서 런타임시 프로그램의 정확한 작동 동작을 지정하여 Haskell 코드의 의미를 설명하고 싶지 않은 대신, 잘못된 계산으로 최저값을 생성하고 해당 값의 동작을 설명합니다. 기본적으로 모든 최하위 값 (기존 값이 아닌)의 모든 속성에 의존해야하는 모든 표현식 최하위 값이됩니다.

"순수한"상태를 유지하려면 최저값을 생성하는 모든 가능한 방법을 동등한 것으로 취급해야합니다. 여기에는 무한 루프를 나타내는 "하단 값"이 포함됩니다. 일부 무한 루프가 실제로 무한 있음을 알 수있는 방법이 없기 때문에, 당신은 검사 할 수 없습니다 (당신은 단지 이상 조금만를 실행 한 경우에는 완료 할 수) 있는 바닥 값의 속성을. 무언가가 바닥에 있는지 테스트 할 수 없으며, 다른 것과 비교할 수 없으며 문자열로 변환 할 수 없으며 아무것도 없습니다. 당신이 할 수있는 일은 손대지 않고 검토되지 않은 장소 (함수 매개 변수, 데이터 구조의 일부 등)입니다.

파이썬은 이미 이런 종류의 바닥을 가지고 있습니다. 예외를 던지거나 종료되지 않는 표현식에서 얻는 "값"입니다. 파이썬은 게으른 것이 아니라 엄격하기 때문에, 그러한 "하단"은 어디에도 저장 될 수 없으며 잠재적으로 검토되지 않은 채 남겨 둘 수 없습니다. 따라서 값을 반환하지 않는 계산이 여전히 값을 가진 것처럼 처리 될 수있는 방법을 설명하기 위해 최저값 개념을 사용할 필요가 없습니다. 그러나 원한다면 예외에 대해 이런 식으로 생각하지 못한 이유도 없습니다.

예외를 던지는 것은 실제로 "순수한"것으로 간주됩니다. 그건 잡기 휴식 순도 그 예외 - 당신이 특정 하단 값에 대한 무언가를 검사 할 수 있습니다 정확하게 있기 때문에, 대신에 상호 교환 그들 모두를 치료의. Haskell IO에서는 불완전한 인터페이스를 허용 하는 예외 만 잡을 수 있습니다 (따라서 보통 상당히 바깥층에서 발생합니다). 파이썬은 순수성을 강요하지는 않지만 어떤 함수가 순수한 함수가 아닌 "외부 불순한 레이어"의 일부인지 스스로 결정할 수 있으며 예외를 잡을 수만 있습니다.

None대신 반품 은 완전히 다릅니다. None하단이 아닌 값입니다. 무언가가 같은지 테스트 할 수 있으며 반환 된 함수의 호출자가 부적절 None하게 사용하여 계속 실행 None됩니다.

따라서 예외를 던질 생각을하고 Haskell의 접근 방식을 모방하기 위해 "하단으로 돌아 가기"를 원한다면 아무 것도하지 않습니다. 예외가 전파되도록합니다. 하스켈 프로그래머가 최저값을 반환하는 함수에 대해 이야기 할 때의 의미입니다.

그러나 기능 프로그래머가 예외를 피하려고 할 때의 의미는 아닙니다 . 기능 프로그래머는 "총 기능"을 선호합니다. 이들은 가능한 모든 입력에 대해 항상 유효하지 않은 리턴 유형의 리턴 유형을 리턴합니다 . 따라서 예외를 던질 수있는 함수는 전체 함수가 아닙니다.

우리가 전체 기능을 좋아하는 이유는 기능을 결합하고 조작 할 때 "블랙 박스"로 취급하기가 훨씬 쉽기 때문입니다. 나는 A 형의 무언가를 받아들이는 타입 A와 전체 기능의 무언가를 반환하는 전체 기능을 가지고 있다면, 나는 알지 못하고, 최초의 출력에서 두 번째를 호출 할 수있는 것도 하나의 구현에 대한을; 나는 함수의 코드가 미래에 어떻게 업데이트되는지에 관계없이 (전체가 유지되고 동일한 유형 서명을 유지하는 한) 유효한 결과를 얻을 수 있다는 것을 알고 있습니다. 이러한 우려의 분리는 리팩토링에 매우 강력한 도움이 될 수 있습니다.

신뢰할 수있는 고차 함수 (다른 함수를 조작하는 함수)에도 다소 필요합니다. 내가 매개 변수로 (알려진 인터페이스) 완전히 임의의 기능을 수신하는 코드를 작성하려는 경우, 나는 내가 입력 오류를 트리거 할 수있는 알 방법이 없기 때문에 블랙 박스로 취급 할 수 있습니다. 총 기능이 주어지면 입력이 없으면 오류가 발생합니다. 마찬가지로 내 고차 함수 호출자는 구현 세부 사항에 의존하지 않는 한 전달하는 함수를 호출하는 데 사용하는 인수를 정확히 알지 못하므로 총 함수를 전달하면 걱정할 필요가 없습니다. 내가하는 일

따라서 예외를 피하도록 조언하는 함수형 프로그래머는 대신 오류 또는 유효한 값을 인코딩하는 값을 반환하는 것이 좋으며이를 사용하려면 두 가지 가능성을 모두 처리해야합니다. 같은 것들 Either유형 또는 Maybe/ Option유형은 단순한 일부 (AN 필요 일반적으로 도움 접착제 함께 물건에 특수 구문 이상 주문 기능을 사용하는 더 강력한 형식의 언어로이 작업을 수행하는 접근이다 A을 생산 것들로 Maybe<A>).

None(오류가 발생한 경우) 또는 일부 값 (오류가없는 경우)을 반환하는 함수 는 위 전략 중 어느 것도 따르지 않습니다 .

오리 타이핑을 사용하는 Python에서는 Either / Maybe 스타일이 많이 사용되지 않고 예외가 발생하는 대신 테스트를 통해 코드의 유형에 따라 함수를 총계로 자동 결합 할 수있는 것이 아니라 코드가 작동하는지 확인합니다. 파이썬은 코드가 Maybe 타입과 같은 것들을 올바르게 사용하도록 강제하는 기능이 없습니다; 당신이 훈련의 문제로 그것을 사용하고 있더라도 실제로 그것을 검증하기 위해 코드를 시험하는 테스트가 필요합니다. 따라서 예외 / 하단 접근 방식은 아마도 파이썬의 순수 기능 프로그래밍에 더 적합 할 것입니다.


1
+1 멋진 답변! 그리고 매우 포괄적입니다.
Andres F.

6

외부 적으로 보이는 부작용이없고 리턴 값이 입력에 전적으로 의존하는 한, 내부적으로 불순한 일이 있더라도 함수는 순수합니다.

따라서 실제로 예외를 발생시킬 수있는 대상에 따라 다릅니다. 경로가 주어진 파일을 열려고하면 파일이 존재하지 않을 수도 있기 때문에 순수하지 않습니다. 이로 인해 동일한 입력에 대해 반환 값이 달라질 수 있습니다.

반면에 주어진 문자열에서 정수를 구문 분석하고 실패하면 예외를 throw하려는 경우 예외가 함수에서 발생하지 않는 한 순수 할 수 있습니다.

참고로, 기능 언어는 가능한 단일 오류 조건이있는 경우에만 단위 유형 을 반환하는 경향이 있습니다. 가능한 오류가 여러 개인 경우 오류에 대한 정보가 포함 된 오류 유형을 반환하는 경향이 있습니다.


하단 유형은 반환 할 수 없습니다.
gardenhead

1
@gardenhead 당신 말이 맞아, 나는 단위 유형을 생각하고 있었다. 결정된.
8bittree

첫 번째 예제의 경우 파일 시스템이 아니며 파일 시스템에 존재하는 파일이 단순히 함수의 입력 중 하나입니까?
Vality

2
@Vaility 실제로는 아마도 파일에 대한 핸들이나 경로가있을 것입니다. 함수 f가 순수하면 f("/path/to/file")항상 같은 값을 반환해야합니다. 실제 파일이 두 번의 호출 사이에서 삭제되거나 변경되면 어떻게됩니까 f?
Andres F.

2
@Vaility 파일에 대한 핸들 대신 파일의 실제 내용 (예 : 정확한 바이트 스냅 샷)을 전달한 경우 함수가 순수 할 수 있습니다.이 경우 동일한 내용이 들어간 경우 동일한 출력이 외출. 그러나 파일에 액세스하는 것은 그렇지 않습니다 :)
Andres F.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.