try/catch idiom in std.datetime
Jonathan M Davis
jmdavisProg at gmx.com
Sun Nov 17 23:28:58 PST 2013
On Sunday, November 17, 2013 22:32:46 Andrei Alexandrescu wrote:
> (It may seem I chose this example to discuss the "let's insert one empty
> line every other line" idiom. I didn't.)
That code's like that just because I like to put empty lines before and after
if statements and before return statements, as I think that that improves
legibility. Short functions like that suffer as a result, because they end up
with a larger proportion of the lines being empty than is normal. I'm always a
bit torn on that, because I don't like having quite that many empty lines in
so few lines, but I also don't like not having space around if statements and
return statements. I never feel like I can find a nice, legible balance with
short functions like that.
> Essentially the code relies on
> calls that may generally throw, but calls them with parameters that
> should guarantee there won't be any throwing. In wanting to offer as
> many "nothrow" guarantees as possible, the code ends up inserting these
> try/catch statements - seemingly needlessly.
Yeah, I tried to use pure and nothrow fairly heavily in std.datetime and ran
into a number of hurdles like this. Fortunately, the situation has improved
somewhat (e.g. format can finally be pure at least some of the time), but it
does show that it's not always easy to use pure or nothrow even when it
arguably should be.
> What would be the best approach here?
>
> 0. Do nothing. This is as good as it gets.
Well, it works just fine as-is, but it would be kind of nice to be able to
solve the problem in a less verbose manner (though you're talking about saving
only a few lines of code).
> 1. Fix scope(failure) and then use it.
Fine with me. I might still favor the try-catch in cases where you can clearly
wrap it around one function call, because then you avoid problems where you've
accidentally effectively marked the whole function as trusted-nothrow when you
only want to mark one function call that way. But you could do the same thing
with scope(failure) and a new scope. The main problem is when you can't really
put the calls that need to be trusted-nothrow inside a new scope, in which
case, you're forced to mark the whole function (or at least large portions of
it) as trusted-nothrow by wrapping it all in a try-catch or scope(failure).
> 2. Relax the nothrow guarantees. After all, nothrow is opt-in.
I'm not quite sure what you're suggesting here. Make it so that nothrow does
checking at runtime instead of compile time? That would be moving in the
direction of C++ and throw specifiers (or more precisely, noexcept, I suppose).
If that's what you're suggesting, I'd be very much against that. I think that
the fact D's nothrow is statically checked is a huge advantage over C++'s
noexcept. The fact that you have to sometimes use try-catch blocks (or
scope(failure) if that works) to make it work is essentially the same as
needing @trusted to make some stuff @safe. I wouldn't want to throw away
@trusted in favor of making @safe more lax either (though that's almost all
static checking which can't be done at runtime, unlike with noexcept).
Of course, there's no way to verify trusted-nothrow except at runtime like
std.datetime is doing with try-catch and assertions, but most code _can_ be
checked statically (including the code that calls the functions that use the
try-catch-assert idiom to be able to be nothrow), and that's much more
pleasant, particularly because it's actually checked by the compiler that way
rather than just blowing up on you at runtime.
I suppose that if it were considered annoying enough to have to use try-catch
blocks or scope(failure), we could add a nothrow equivalent to @trusted to
mark functions with, though it's already been argued that @trusted should be
on pieces of a function rather than on the whole function, and it would
arguably be better to mark sections of a function as trusted-nothrow rather
than the entire thing. try-catch lets us do that already, but it might be nice
to be able to do the equivalent of
@property FracSec fracSec() const nothrow
{
trusted-nothrow
{
auto hnsecs = removeUnitsFromHNSecs!"days"(adjTime);
if(hnsecs < 0)
hnsecs += convert!("hours", "hnsecs")(24);
hnsecs = removeUnitsFromHNSecs!"seconds"(hnsecs);
return FracSec.from!"hnsecs"(cast(int)hnsecs);
}
}
and have the compiler insert the catch and assertion for you.
> 3. Look into API changes that add nothrow, narrower functions to the
> more general ones that may throw.
In some cases (e.g. format) that's probably a good idea, but I don't think
that really scales. In some cases, it would be well worth it, whereas in
others, it would just be better to use try-catch in the few places where you
know the function won't throw, because it would be quite rare to be able to
guarantee that it wouldn't throw. It's also not pleasant to have to duplicate
functions all over the place.
> 4. ...?
We now have std.exception.assumeWontThrow, which works reasonably well when
you need to wrap a single call as opposed to several, but it has the same
problem as enforce in that it uses lazy, which is definite performance hit. So,
in most cases, I'd be more inclined to just use a try-catch, and if it's more
than one expression, you pretty much need to use try-catch or scope(failure)
instead anyway, since you wouldn't want to wrap whole function bodies in a
call to assumeWontThrow (assuming that you even could).
So, that's a partial solution, but not a perfomant one. However, we do really
need to improve the performance of lazy, because enforce gets used all over
the place, and it's definitely shown up as being costly in some of the profiling
that's been shown in the newsgroup. And if that gets fixed, then using
assumeWontThrow wouldn't be as bad.
All in all, I find the need to use try-catch blocks to effectively do trusted-
nothrow a bit annoying, but I haven't felt that it was a big enough deal to
try and find another solution for it. Having to do trusted-purity is far worse,
because that requires using a function pointer and casting it, but that would
_should_ be hard to do because of how hard it is to actually guarantee that
the function is acting like a pure function even though it isn't. And I'm not
sure that I'd entirely trust myself with that, let alone the average D
developer. trusted-nothrow on the other hand is something that the average
programmer should be able to grasp.
- Jonathan M Davis
More information about the Digitalmars-d
mailing list