Failure — 지연된 예외

Failure — 지연된 예외

에러가 났을 때 꼭 그 자리에서 터지지 않고, 값처럼 나중에 처리되길 바랄 때가 있어요. 그런 "아직 던져지지 않은 부드러운 예외"를 나타내는 타입이 Failure예요. 보통 &fail을 호출해 만들어져요.

출처: Raku Documentation — Failure

본문

class Failure is Nil { }

Failure는 부드러운(soft), 즉 던져지지 않은 Exception으로, 보통 &fail을 호출해 생성돼요. Exception 객체를 감싸는 래퍼처럼 동작해요.

싱크(void) 문맥은 Failure가 던져지게, 즉 일반 예외로 바뀌게 만들어요. use fatal 프래그마를 쓰면 그 프래그마 범위 안의 모든 문맥에서 그렇게 돼요. try 블록 안에서는 use fatal이 자동으로 설정되고, no fatal로 끌 수 있어요.

Failure는 주로 정상적으로 rvalue를 만들어내는 코드에서 유용해요. 싱크 문맥(즉 say처럼 부수 효과를 위해 자주 호출되는 코드)에서 FailureException과 거의 동등해져요.

마찬가지로, 일반적으로 &fail은 뭔가를 반환하길 기대하는 코드 안에서만 써야 해요.

Failure를 진리값으로 검사하거나(Bool 메서드) 정의 여부를 검사하면(defined 메서드) 그 실패가 "처리됨(handled)"으로 표시되고, 이제 싱크 문맥에서 던져지지 않아요.

handled 메서드를 호출하면 실패가 처리되었는지 확인할 수 있어요.

처리되지 않은 실패에 메서드를 호출하면 실패가 전파돼요. 명세는 결과가 또 다른 Failure라고 말하지만, Rakudo에서는 실패가 던져지게 해요.

Failure는 정의되지 않은 Nil이기 때문에, 실패할 수 있는 코드를 안전하게 실행하는 흔한 관용구는 with/else 문을 쓰는 거예요.

sub may_fail( --> Numeric:D ) {
  my $value = (^10).pick || fail "Zero is unacceptable";
  fail "Odd is also not okay" if $value % 2;
  return $value;
}

with may_fail() -> $value { # defined, so didn't fail
  say "I know $value isn't zero or odd."
} else { # undefined, so failed, and the Failure is the topic
  say "Uh-oh: {.exception.message}."
}

method new

multi method new(Failure:D:)
multi method new(Failure:U:)
multi method new(Failure:U: Exception:D \exception)
multi method new(Failure:U: $payload)
multi method new(Failure:U: |cap (*@msg))

인자로 주어진 payload로 새 Failure 인스턴스를 반환해요. Failure 객체에 인자 없이 호출하면 던져지고, 타입 값에 호출하면 payload가 없는 빈 Failure를 만들어요. 후자의 payload는 Exception이거나 Exception용 payload일 수 있어요. 전형적인 payload는 오류 메시지를 담은 Str이에요. payload 리스트도 받아들여져요.

my $e = Failure.new(now.DateTime, 'WELP‼');
say $e;
CATCH{ default { say .^name, ': ', .Str } }
# OUTPUT: «X::AdHoc: 2017-09-10T11:56:05.477237ZWELP‼␤»

method handled

method handled(Failure:D: --> Bool:D) is rw

처리된 실패면 True, 아니면 False를 반환해요.

sub f() { fail }; my $v = f; say $v.handled; # OUTPUT: «False␤»

handled 메서드는 lvalue예요(참고: routine trait is rw). 그래서 handled 상태를 설정하는 데 쓸 수도 있어요.

sub f() { fail }
my $v = f;
$v.handled = True;
say $v.handled; # OUTPUT: «True␤»

method exception

method exception(Failure:D: --> Exception)

실패가 감싸고 있는 Exception 객체를 반환해요.

sub failer() { fail };
my $failure = failer;
my $ex = $failure.exception;
put "$ex.^name(): $ex";
# OUTPUT: «X::AdHoc: Failed␤»

method self

method self(Failure:D: --> Failure:D)

invocant가 처리된 Failure면 그대로 반환해요. 처리되지 않았다면 그 Exception을 던져요. Mu 타입이 모든 클래스에 .self를 제공하므로, 이 메서드를 호출하는 것은 Failure를 폭발적으로 걸러내는 편리한 방법이에요.

my $num1 = '♥'.Int;
# $num1 now contains a Failure object, which may not be desirable

my $num2 = '♥'.Int.self;
# .self method call on Failure causes an exception to be thrown

my $num3 = '42'.Int.self;
# Int type has a .self method, so here $num3 has `42` in it

(my $stuff = '♥'.Int).so;
say $stuff.self; # OUTPUT: «(HANDLED) Cannot convert string to number…»
# Here, Failure is handled, so .self just returns it as is

method Bool

multi method Bool(Failure:D: --> Bool:D)

False를 반환하고 실패를 처리된 것으로 표시해요.

sub f() { fail };
my $v = f;
say $v.handled; # OUTPUT: «False␤»
say $v.Bool;    # OUTPUT: «False␤»
say $v.handled; # OUTPUT: «True␤»

method Capture

method Capture()

invocant가 타입 객체이거나 처리된 FailureX::Cannot::Capture를 던져요. 그렇지 않으면 invocant의 exception을 던져요.

method defined

multi method defined(Failure:D: --> Bool:D)

False를 반환하고(실패는 공식적으로 정의되지 않음) 실패를 처리된 것으로 표시해요.

sub f() { fail };
my $v = f;
say $v.handled; # OUTPUT: «False␤»
say $v.defined; # OUTPUT: «False␤»
say $v.handled; # OUTPUT: «True␤»

method list

multi method list(Failure:D:)

실패를 처리된 것으로 표시하고 invocant의 exception을 던져요.

sub fail

multi fail(--> Nil)
multi fail(*@text)
multi fail(Exception:U $e  --> Nil )
multi fail($payload --> Nil)
multi fail(|cap (*@msg) --> Nil)
multi fail(Failure:U $f --> Nil)
multi fail(Failure:D $fail --> Nil)

호출한 루틴을 빠져나가고, $e를 감싼 Failure 객체—또는 cap이나 $payload 형식이면 @text의 연결로 만들어진 X::AdHoc 예외—를 반환해요. 호출자가 use fatal; 프래그마로 치명적 예외를 활성화했다면, Failure로 반환하는 대신 예외가 던져져요.

# A custom exception defined
class ForbiddenDirectory is Exception {
    has Str $.name;

    method message { "This directory is forbidden: '$!name'" }
}

sub copy-directory-tree ($dir) {
    # We don't allow for non-directories to be copied
    fail "$dir is not a directory" if !$dir.IO.d;
    # We don't allow 'foo' directory to be copied too
    fail ForbiddenDirectory.new(:name($dir)) if $dir eq 'foo';
    # or above can be written in method form as:
    # ForbiddenDirectory.new(:name($dir)).fail if $dir eq 'foo';
    # Do some actual copying here
    ...
}

# A Failure with X::AdHoc exception object is returned and
# assigned, so no throwing Would be thrown without an assignment
my $result = copy-directory-tree("cat.jpg");
say $result.exception; # OUTPUT: «cat.jpg is not a directory␤»

# A Failure with a custom Exception object is returned
$result = copy-directory-tree('foo');
say $result.exception; # OUTPUT: «This directory is forbidden: 'foo'␤»

일반적인(undefined) Failure로 호출하면 임시(ad-hoc)의 정의되지 않은 실패가 던져지고, 정의된 Failure면 처리되지 않은 것으로 표시돼요.

sub re-fail {
    my $x = +"a";
    unless $x.defined {
        $x.handled = True;
        say "Something has failed in \$x ", $x.^name;
        # OUTPUT: «Something has failed in $x Failure␤»
        fail($x);
        return $x;
    }
}

my $x = re-fail;
say $x.handled; # OUTPUT: «False␤»

출처: Raku Documentation — Failure