crystal icon indicating copy to clipboard operation
crystal copied to clipboard

Safer version of methods with `raise`

Open cjgajard opened this issue 9 years ago • 6 comments

I'm following the discussion of #3371 , but from a different and (to me) more Crystal-style point of view.

With trusted sources there's no problem of Exceptions, even more, allowing simpler code is great, but exceptions are bad when dealing with untrusted sources, they are a totally avoidable performance penalty.

My propose, just like when (unsafe [] + safe []?) was preferred over (safe [] + unsafe []!), is having a "safer" versions of unsafe methods, which return a union of (T | ErrorUsingStackMemory). Additionally returning more than 1 error type should be supported too.

I imagine something using a ! convention:

name return check handle
foo T run-time rescue
foo? `(T Nil)` compile-time
foo! `(T Error)` run-time, ensuring handle of all braches at compile-time

TLDR; I don't what to be severe everywhere, but when I do, I don't want performance penalty and I want to use unions

PS: Having errors duplicated because of this can be a problem... The best thing I can think of now is having a convention with FooException for raise and FooError for union return.

What do you think? And thanks for reading :sweat_smile:

cjgajard avatar Oct 09 '16 06:10 cjgajard

This is not exactly related, but, In my opinion, when a func can return T|Nil and has alternative that raises - then they should be named foo? and foo! respectively. That is: foo should not exists. For "lone" foo - I expect it to return T, or when results may be nil too: T|Nil. But not raise. So foo raises, foo? returns T|Nil feels "dangerous" imo.

Imagine using a class, and lib-author has defined getter on an ivar. Then the lib-author later change it to getter! in an update. Unexpected things will happen in your code. (Of course - such a change should be covered by semver major bump - but... Reality == Mistakes)

ozra avatar Oct 09 '16 17:10 ozra

If this is simply a performance issue, is there any fundamental reason why exceptions have to be implemented by stack unwinding (obviously for printing stacktraces the stack has to be unwound) and not a more performant method? Crystal has the benefit of being able to see the program as a whole, and possibly reduce or remove exception throwing overhead (although I might be missing some fundamental roadblock), so if that was the case, would explicit return of errors be useful any more?

RX14 avatar May 14 '17 13:05 RX14

Being able to return Foo|Err, where Err is defined to be "falsey" at class-method level would be awesome (allowing generalizing the Foo|Nil conditional check type narrowing, allowing, perhaps even: Any|Types|AsLong|AsOnlyOne|IsTruthy - although that would not likely be a real world scenario: handling just a two-type-union would be good enough:

if x = maybe-foo()
   p "x is a Foo here"
else
   p "x is a Err here"

That is, if x is "truthy", we know it's the type that is truthy, since the other is defined as being a "falsey type", likewise, x is narrowed to the other type in the other branch.

As mentioned above, and in other issues I believe, when failures are expected to happen - this would be the clean (and more performant) way to implement the code rather than by throwing exceptions - with should be _exceptional.

ozra avatar May 17 '17 17:05 ozra

Likely not going to change. For now, we are free to use unions and block yielding as alternative of exceptions. Besides performance, which should not be that huge deal since exceptions should be... exceptional, and Crystal being fast, one disadvantage of exceptions is knowing all possible errors of a method.

I propose to close this issue, such discussion can now go to the forum.

j8r avatar Feb 12 '21 22:02 j8r

By the way, if you want to be absolutely safe the correct return type is a disjoint union such as {ok: T} | {err: E}, especially in situations where nil is a valid return value (e.g. Array(String?)#find). Then in an ideal world we would pattern-match on a NamedTuple's keys instead of augmenting the truthiness semantics.

Crystal also doesn't aim for bare-bones performance at the moment, compared to certain other LLVM-based languages.

HertzDevil avatar Feb 12 '21 23:02 HertzDevil

exceptions are bad when dealing with untrusted sources, they are a totally avoidable performance penalty.

This premise isn't entirely true: performance of different error handling mechanisms depends a lot on specific circumstances, such as the ratio between success and error cases, for example. Exceptions are actually quite performant for the success path and only introduce overhead when an exception is raised. Errors in return values on the other hand require a status check every time in order to determine whether the result is a success value or an error. And this may happen in multiple locations in the call stack. (read more: https://purplesyringa.moe/blog/you-might-want-to-use-panics-for-error-handling/)

From a performance point of view, I think a good rule of thumb is that exceptions are best for errors which are expected to occur very rarely in comparison to a happy result. Error return values are better for errors that are expected to happen more frequently.

straight-shoota avatar Sep 16 '24 13:09 straight-shoota