digitalmars.D - Let's improve D's exceptions
- Adam D. Ruppe (77/77) May 13 2015 Have you ever done:
- Leandro Motta Barros via Digitalmars-d (5/71) May 13 2015 I didn't give much attention to the details. but I like the spirit very
- Jacob Carlborg (9/23) May 13 2015 Yeah, I really hate that people are using plain Exception instead of
- weaselcat (5/8) May 13 2015 With how range focused/@nogc a lot of phobos code has become
- Steven Schveighoffer (25/51) May 13 2015 enforce is one of the most needless pieces of phobos:
- Jacob Carlborg (10/34) May 14 2015 Now you're back to the same problem as "enforce" has. That it throws
- Steven Schveighoffer (7/42) May 14 2015 Sure, but I wasn't arguing about that. I just don't like the utility of
- Atila Neves (12/77) May 18 2015 One thing I like about `enforce` is that the program's run-time
- Steven Schveighoffer (12/18) May 18 2015 Yes, but that's a minor nuisance. I often do not rewrite conditions by
- Dmitry Olshansky (4/19) May 18 2015 Perl's unless would "solve" this nicely ;)
- Meta (5/9) May 19 2015 I disagree; I also heavily use enforce, and I find the overload
- Adam D. Ruppe (32/35) May 13 2015 Aye. enforce is one of those things that looks cool but I don't
- Jacob Carlborg (5/8) May 14 2015 It was a while since I looked at that DIP, but I'm mostly interested in
- Adam D. Ruppe (6/8) May 14 2015 I think hierarchies will get better too if there were more
- Jacob Carlborg (5/8) May 15 2015 So why isn't data members used more, because of the boilerplate to
- Adam D. Ruppe (13/15) May 15 2015 Yeah, I think so. A new exception subclass has to write out the
- "Ola Fosheim =?UTF-8?B?R3LDuHN0YWQi?= (6/10) May 14 2015 Using alias like this makes code hard to read. Error types should
- Adam D. Ruppe (9/14) May 14 2015 Perhaps, I don't hate it too much here though because the alias
- Marco Leise (10/28) May 18 2015 Yep, implemented something like that too. I worry about the
- Kagamin (3/5) May 14 2015 Maybe also replace file name with ModuleInfo similar to how
- w0rp (5/5) May 14 2015 I wonder if enforce should throw an Error instead, if it exists
- Adam D. Ruppe (3/6) May 14 2015 Is it really? My thought of enforce was it iwas just a lazy way
- Jacob Carlborg (5/7) May 15 2015 Yeah, that's what I thought too. The documentation doesn't mention
- Adam D. Ruppe (6/8) May 14 2015 Those bug me because all it really wants from it is the name and
- Kagamin (5/13) May 14 2015 ModuleInfo is primarily to extract module name and use it instead
- Dicebot (4/4) May 19 2015 For me it feels like until the issue with non-allocating
Have you ever done: if(something) { import std.conv; throw new Exception("some error " ~ to!string(some_value)); } Don't you hate it? * having to import std.conv to see data from your exception is a pain * it allocates eagerly and thus isn't suitable for a lot of places * inspecting the data can be a pain as the string is unstructured This assumes the data is even bothered to be added. Anyone who has gotten a RangeError in D knows important information is often just dropped! A good solution is to make a new exception subclass for each error type, storing details as data members. However, that's a bit of a pain in D because of all the work you have to do to make a complete subclass: * you need to forward the constructors * you need to reimplement the toString to print out data members * you still have a string member there that is encouraged to be used Finally, with the discussion of nogc exceptions, it would be nice to simplify the constructor and get file/line info separated from it at the same time too. Can we tackle all these problems at once? I propose we do some library changes, one minor compiler change, and a culture shift toward better exceptions. Here's my proof of concept: http://arsdnet.net/dcode/exception.d Read that code and the comments to see what I'm going for and what it looks like. Here's the summary: * The Exception constructor is simplified: no required arguments, everything else is done with members. (The Throwable next = null argument might come back, but it can also be set after construction anyway so I'd prefer not to worry about it in most places either.) * The string is de-emphasized. It is still there so we don't break all the code that uses it now, but it is no longer the main thing and we really would prefer that it isn't used at all. Exception messages are instead done with the class name and data members. The string may be gotten by the class though in cases like converting errno to a message. * File and line is set at the throw point instead of the construction point. * A mixin template uses reflection to print data to the user instead of constructing a string. * This is allocation-free, except for the exception object itself. User code would NOT have to import std.conv, it just stores the data in the object. Changes: TO THE COMPILER: * make the call to d_throw add file and line number information. This is completely backward compatible and allows the runtime to store it in the object if it wants to. Then my "fly" method becomes unnecessary. TO THE LIBRARY: * Throwable's toString implementation changes a bit to call a couple more virtual functions to print details. Very similar to the current code, as you can see in my file there, just some overridable hooks. * A PrintMembers and perhaps other helper mixins are added to the library. Doesn't matter where they go, druntime might do a simpler one for its own needs and then Phobos offers a full featured one for user code. Or whatever. It'd need to be a bit more complex than my proof of concept to cover more data (at least let it try calling toString on types too) but this is already pretty useful as is. TO USER CODE: * We start using subclasses with data members instead of string arguments, migrating to it incrementally and encouraging people to write exceptions this way going forward. * Note that this should be completely backward compatible, it won't break old code, just encourage a new better way. What do you all think? I encourage you to grab my example there and play with it a bit to see how you like it too and see if you can think of any improvements. D's exceptions kinda suck right now but they don't have to!
May 13 2015
I didn't give much attention to the details. but I like the spirit very much. LMB On Wed, May 13, 2015 at 12:08 PM, Adam D. Ruppe via Digitalmars-d < digitalmars-d puremagic.com> wrote:Have you ever done: if(something) { import std.conv; throw new Exception("some error " ~ to!string(some_value)); } Don't you hate it? * having to import std.conv to see data from your exception is a pain * it allocates eagerly and thus isn't suitable for a lot of places * inspecting the data can be a pain as the string is unstructured This assumes the data is even bothered to be added. Anyone who has gotten a RangeError in D knows important information is often just dropped! A good solution is to make a new exception subclass for each error type, storing details as data members. However, that's a bit of a pain in D because of all the work you have to do to make a complete subclass: * you need to forward the constructors * you need to reimplement the toString to print out data members * you still have a string member there that is encouraged to be used Finally, with the discussion of nogc exceptions, it would be nice to simplify the constructor and get file/line info separated from it at the same time too. Can we tackle all these problems at once? I propose we do some library changes, one minor compiler change, and a culture shift toward better exceptions. Here's my proof of concept: http://arsdnet.net/dcode/exception.d Read that code and the comments to see what I'm going for and what it looks like. Here's the summary: * The Exception constructor is simplified: no required arguments, everything else is done with members. (The Throwable next = null argument might come back, but it can also be set after construction anyway so I'd prefer not to worry about it in most places either.) * The string is de-emphasized. It is still there so we don't break all the code that uses it now, but it is no longer the main thing and we really would prefer that it isn't used at all. Exception messages are instead done with the class name and data members. The string may be gotten by the class though in cases like converting errno to a message. * File and line is set at the throw point instead of the construction point. * A mixin template uses reflection to print data to the user instead of constructing a string. * This is allocation-free, except for the exception object itself. User code would NOT have to import std.conv, it just stores the data in the object. Changes: TO THE COMPILER: * make the call to d_throw add file and line number information. This is completely backward compatible and allows the runtime to store it in the object if it wants to. Then my "fly" method becomes unnecessary. TO THE LIBRARY: * Throwable's toString implementation changes a bit to call a couple more virtual functions to print details. Very similar to the current code, as you can see in my file there, just some overridable hooks. * A PrintMembers and perhaps other helper mixins are added to the library. Doesn't matter where they go, druntime might do a simpler one for its own needs and then Phobos offers a full featured one for user code. Or whatever. It'd need to be a bit more complex than my proof of concept to cover more data (at least let it try calling toString on types too) but this is already pretty useful as is. TO USER CODE: * We start using subclasses with data members instead of string arguments, migrating to it incrementally and encouraging people to write exceptions this way going forward. * Note that this should be completely backward compatible, it won't break old code, just encourage a new better way. What do you all think? I encourage you to grab my example there and play with it a bit to see how you like it too and see if you can think of any improvements. D's exceptions kinda suck right now but they don't have to!
May 13 2015
On 2015-05-13 17:08, Adam D. Ruppe wrote:Have you ever done: if(something) { import std.conv; throw new Exception("some error " ~ to!string(some_value)); } Don't you hate it? * having to import std.conv to see data from your exception is a pain * it allocates eagerly and thus isn't suitable for a lot of places * inspecting the data can be a pain as the string is unstructured This assumes the data is even bothered to be added. Anyone who has gotten a RangeError in D knows important information is often just dropped! A good solution is to make a new exception subclass for each error type, storing details as data members. However, that's a bit of a pain in D because of all the work you have to do to make a complete subclass:Yeah, I really hate that people are using plain Exception instead of creating a subclass. I'm trying to point this out in pull requests and similar but it's hard to get people to listen. One thing that is _not_ making things better is "enforce" which, if I recall correctly, throws Exception by default. I think this is related: http://wiki.dlang.org/DIP33 -- /Jacob Carlborg
May 13 2015
On Wednesday, 13 May 2015 at 19:24:09 UTC, Jacob Carlborg wrote:Yeah, I really hate that people are using plain Exception instead of creating a subclass. I'm trying to point this out in pull requests and similar but it's hard to get people to listen.With how range focused/ nogc a lot of phobos code has become exceptions have almost become useless, and we're back to throwing errors anyways. Bad situation.
May 13 2015
On 5/13/15 3:24 PM, Jacob Carlborg wrote:On 2015-05-13 17:08, Adam D. Ruppe wrote:enforce is one of the most needless pieces of phobos: enforce(cond, message); vs. if(!cond) throw new Exception(message); And the second doesn't mess up inlining. I think enforce could be boiler-plated better. The only verbose part of the if version is the throwing and newing. template throe(Etype = Exception) { void throe(Args...)(Args args, string file = __FILE__, size_t line = __LINE__) { throw new Etype(args, file, line); } } if(!cond) throe(message); Wait, you're in an io package, and you want to always throw IO exceptions? alias except = throe!IOException; if(!cond) except(args, to, ioexception); Sure, it doesn't return the thing that caused the exception if nothing happens. Grepping phobos, this feature is used with enforce about 1% of the time. In fact, I didn't even know it had that feature until looking it up in the docs just now. -SteveHave you ever done: if(something) { import std.conv; throw new Exception("some error " ~ to!string(some_value)); } Don't you hate it? * having to import std.conv to see data from your exception is a pain * it allocates eagerly and thus isn't suitable for a lot of places * inspecting the data can be a pain as the string is unstructured This assumes the data is even bothered to be added. Anyone who has gotten a RangeError in D knows important information is often just dropped! A good solution is to make a new exception subclass for each error type, storing details as data members. However, that's a bit of a pain in D because of all the work you have to do to make a complete subclass:Yeah, I really hate that people are using plain Exception instead of creating a subclass. I'm trying to point this out in pull requests and similar but it's hard to get people to listen. One thing that is _not_ making things better is "enforce" which, if I recall correctly, throws Exception by default.
May 13 2015
On 2015-05-14 00:55, Steven Schveighoffer wrote:enforce is one of the most needless pieces of phobos: enforce(cond, message); vs. if(!cond) throw new Exception(message); And the second doesn't mess up inlining. I think enforce could be boiler-plated better. The only verbose part of the if version is the throwing and newing. template throe(Etype = Exception) { void throe(Args...)(Args args, string file = __FILE__, size_t line = __LINE__) { throw new Etype(args, file, line); } } if(!cond) throe(message);Now you're back to the same problem as "enforce" has. That it throws Exception by default. It shouldn't have a default value for the exception type. BTW, it could be called "raise" as it's called in some other languages.Wait, you're in an io package, and you want to always throw IO exceptions? alias except = throe!IOException; if(!cond) except(args, to, ioexception);That is a bit better but I still think that IOException is too generic. Streaming something over the network and trying to open a file is quite different.Sure, it doesn't return the thing that caused the exception if nothing happens. Grepping phobos, this feature is used with enforce about 1% of the time. In fact, I didn't even know it had that feature until looking it up in the docs just now. -Steve-- /Jacob Carlborg
May 14 2015
On 5/14/15 6:12 AM, Jacob Carlborg wrote:On 2015-05-14 00:55, Steven Schveighoffer wrote:Sure, but I wasn't arguing about that. I just don't like the utility of enforce completely -- it provides very little value, and kills inlining.enforce is one of the most needless pieces of phobos: enforce(cond, message); vs. if(!cond) throw new Exception(message); And the second doesn't mess up inlining. I think enforce could be boiler-plated better. The only verbose part of the if version is the throwing and newing. template throe(Etype = Exception) { void throe(Args...)(Args args, string file = __FILE__, size_t line = __LINE__) { throw new Etype(args, file, line); } } if(!cond) throe(message);Now you're back to the same problem as "enforce" has. That it throws Exception by default. It shouldn't have a default value for the exception type.BTW, it could be called "raise" as it's called in some other languages.My symbol wasn't intended as a proposal, just something that sounds like "throw" but without using the keyword :) raise would be fine.It was just an example, you can use whatever exception you want. -SteveWait, you're in an io package, and you want to always throw IO exceptions? alias except = throe!IOException; if(!cond) except(args, to, ioexception);That is a bit better but I still think that IOException is too generic. Streaming something over the network and trying to open a file is quite different.
May 14 2015
One thing I like about `enforce` is that the program's run-time checks become positive instead of negative statements which I think is a lot more readable. i.e. enforce(foo && bar > 4, "..."); instead of if(!foo || bar <=3) throw new Exception("..."); I agree with Adam that importing `std.conv` everytime for `text` is annoying, as is the whole subclassing and forwarding constructor parameters. Atila On Wednesday, 13 May 2015 at 22:55:22 UTC, Steven Schveighoffer wrote:On 5/13/15 3:24 PM, Jacob Carlborg wrote:On 2015-05-13 17:08, Adam D. Ruppe wrote:enforce is one of the most needless pieces of phobos: enforce(cond, message); vs. if(!cond) throw new Exception(message); And the second doesn't mess up inlining. I think enforce could be boiler-plated better. The only verbose part of the if version is the throwing and newing. template throe(Etype = Exception) { void throe(Args...)(Args args, string file = __FILE__, size_t line = __LINE__) { throw new Etype(args, file, line); } } if(!cond) throe(message); Wait, you're in an io package, and you want to always throw IO exceptions? alias except = throe!IOException; if(!cond) except(args, to, ioexception); Sure, it doesn't return the thing that caused the exception if nothing happens. Grepping phobos, this feature is used with enforce about 1% of the time. In fact, I didn't even know it had that feature until looking it up in the docs just now. -SteveHave you ever done: if(something) { import std.conv; throw new Exception("some error " ~ to!string(some_value)); } Don't you hate it? * having to import std.conv to see data from your exception is a pain * it allocates eagerly and thus isn't suitable for a lot of places * inspecting the data can be a pain as the string is unstructured This assumes the data is even bothered to be added. Anyone who has gotten a RangeError in D knows important information is often just dropped! A good solution is to make a new exception subclass for each error type, storing details as data members. However, that's a bit of a pain in D because of all the work you have to do to make a complete subclass:Yeah, I really hate that people are using plain Exception instead of creating a subclass. I'm trying to point this out in pull requests and similar but it's hard to get people to listen. One thing that is _not_ making things better is "enforce" which, if I recall correctly, throws Exception by default.
May 18 2015
On 5/18/15 8:20 AM, Atila Neves wrote:One thing I like about `enforce` is that the program's run-time checks become positive instead of negative statements which I think is a lot more readable. i.e. enforce(foo && bar > 4, "..."); instead of if(!foo || bar <=3) throw new Exception("...");Yes, but that's a minor nuisance. I often do not rewrite conditions by distributing the negation to avoid this problem. In that case, the "operation" becomes basically: if(!(...)) We could introduce a new 'when' keyword: when(...) joking, joking!!! don't start flaming :) Alternatively, we could fix lazy parameters so they don't disable inlining (though the weight carried by enforce is so minor, I still don't think it's worth having). -Steve
May 18 2015
On 18-May-2015 17:18, Steven Schveighoffer wrote:On 5/18/15 8:20 AM, Atila Neves wrote:Perl's unless would "solve" this nicely ;) -- Dmitry OlshanskyOne thing I like about `enforce` is that the program's run-time checks become positive instead of negative statements which I think is a lot more readable. i.e. enforce(foo && bar > 4, "..."); instead of if(!foo || bar <=3) throw new Exception("...");Yes, but that's a minor nuisance. I often do not rewrite conditions by distributing the negation to avoid this problem. In that case, the "operation" becomes basically: if(!(...)) We could introduce a new 'when' keyword:
May 18 2015
On Monday, 18 May 2015 at 14:18:46 UTC, Steven Schveighoffer wrote:Alternatively, we could fix lazy parameters so they don't disable inlining (though the weight carried by enforce is so minor, I still don't think it's worth having). -SteveI disagree; I also heavily use enforce, and I find the overload that returns a value particularly useful for reducing indentation in my programs.
May 19 2015
On Wednesday, 13 May 2015 at 19:24:09 UTC, Jacob Carlborg wrote:One thing that is _not_ making things better is "enforce" which, if I recall correctly, throws Exception by default.Aye. enforce is one of those things that looks cool but I don't think should actually be used. It could perhaps be fixed... alias enforce = enforceBase!FileException; FILE* fp = enforce!fopen("test.txt", "rt"); The local alias tells which kind of exception is relevant in this context. Then the local enforce collects the arguments to the function and throws a new subclass of the base specific to this function call. There's arguably value in that, but still, I'm not sure it is better than just doing it yourself. Refresh this: http://arsdnet.net/dcode/exception.d It now has my enforce 2.0 proof of concept draft. Usage: alias enforce = enforceBase!MyExceptionBase; import core.stdc.stdio; enforce!fopen("nofile.txt".ptr, "rb".ptr); Message: MyExceptionBase exception.d(38): fopen call failed filename = nofile.txt mode = rb ---------------- stack trace here That's actually kinda cool, automatically populating the exception with the arguments to the call. Perhaps of some value (and I don't think it will break inline since it uses a function alias argument instead of lazy params!)I think this is related: http://wiki.dlang.org/DIP33Yeah, we could use a decent hierarchy too. Though the examples in there still use string concatenation to form the message, which is the big thing I want to get away from.
May 13 2015
On 2015-05-14 03:31, Adam D. Ruppe wrote:Yeah, we could use a decent hierarchy too. Though the examples in there still use string concatenation to form the message, which is the big thing I want to get away from.It was a while since I looked at that DIP, but I'm mostly interested in the hierarchy. -- /Jacob Carlborg
May 14 2015
On Thursday, 14 May 2015 at 10:08:36 UTC, Jacob Carlborg wrote:It was a while since I looked at that DIP, but I'm mostly interested in the hierarchy.I think hierarchies will get better too if there were more incentive to use them - data members instead of string concat encourages new classes and you want to inherit from something... then if there's not a million lines of boilerplate for a new class, it gets to be a lot easier to do.
May 14 2015
On 2015-05-14 14:33, Adam D. Ruppe wrote:I think hierarchies will get better too if there were more incentive to use them - data members instead of string concat encourages new classes and you want to inherit from somethingSo why isn't data members used more, because of the boilerplate to create a new subcalss? -- /Jacob Carlborg
May 15 2015
On Friday, 15 May 2015 at 09:23:12 UTC, Jacob Carlborg wrote:So why isn't data members used more, because of the boilerplate to create a new subcalss?Yeah, I think so. A new exception subclass has to write out the long constructor signature, forward it, add the data members, write code to set them, and at least write code to add them to the toString (which itself has a long signature and needs a lot of string formatting tedium). That's a fair amount of code to write, especially compared to new Exception("foo " ~ to!string(value)) right there inline when it comes up. But with reflection we can really simplify it to the point where writing class Foo : Exception { int value; mixin ExtendException; } becomes less of a hassle, or we could even extend a hierarchy right inline like raise!SomeException(variadic, arguments).
May 15 2015
On Thursday, 14 May 2015 at 01:31:22 UTC, Adam D. Ruppe wrote:The local alias tells which kind of exception is relevant in this context. Then the local enforce collects the arguments to the function and throws a new subclass of the base specific to this function call.Using alias like this makes code hard to read. Error types should be humanly deducible at the failure site. You'd be better off having non-ignorable result types (e.g. tagged union/variant/algebraic) and a typed way to turn those into exceptions.
May 14 2015
On Thursday, 14 May 2015 at 10:24:45 UTC, Ola Fosheim Grøstad wrote:Using alias like this makes code hard to read. Error types should be humanly deducible at the failure site.Perhaps, I don't hate it too much here though because the alias is fairly local. Without it, you'd probably write enforce!(BaseClass, fopen)("file", "rb") to get the thing I was going for.You'd be better off having non-ignorable result types (e.g. tagged union/variant/algebraic) and a typed way to turn those into exceptions.The only place where I'd be interested in using something like this is calling F functions and you can't change their signature too much...
May 14 2015
Am Thu, 14 May 2015 01:31:21 +0000 schrieb "Adam D. Ruppe" <destructionator gmail.com>:Refresh this: http://arsdnet.net/dcode/exception.d It now has my enforce 2.0 proof of concept draft. Usage: alias enforce = enforceBase!MyExceptionBase; import core.stdc.stdio; enforce!fopen("nofile.txt".ptr, "rb".ptr); Message: MyExceptionBase exception.d(38): fopen call failed filename = nofile.txt mode = rb ---------------- stack trace hereYep, implemented something like that too. I worry about the code bloat, but it sure is powerful. It did fail with vararg C functions though :p nogc Exceptions would also be nice in my opinion. I can agree with your ideas, although I never found the Throwable really bad. It gets the job done. -- Marco
May 18 2015
On Wednesday, 13 May 2015 at 15:08:58 UTC, Adam D. Ruppe wrote:* File and line is set at the throw point instead of the construction point.Maybe also replace file name with ModuleInfo similar to how assert works?
May 14 2015
I wonder if enforce should throw an Error instead, if it exists at all. Because it's designed to throw an exception you shouldn't catch. If you are going to have it throw an Exception subclass, then it should take the exception type, like enforce!WhateverException(...), or something.
May 14 2015
On Thursday, 14 May 2015 at 12:26:45 UTC, w0rp wrote:I wonder if enforce should throw an Error instead, if it exists at all. Because it's designed to throw an exception you shouldn't catch.Is it really? My thought of enforce was it iwas just a lazy way to throw on cases like file not found...
May 14 2015
On 2015-05-14 14:41, Adam D. Ruppe wrote:Is it really? My thought of enforce was it iwas just a lazy way to throw on cases like file not found...Yeah, that's what I thought too. The documentation doesn't mention anything about this. -- /Jacob Carlborg
May 15 2015
On Thursday, 14 May 2015 at 10:29:57 UTC, Kagamin wrote:Maybe also replace file name with ModuleInfo similar to how assert works?Those bug me because all it really wants from it is the name and then you need all the moduleinfo even in bare metal. Exception support requires some RTTI anyway so maybe it doesn't matter, but my playing with bare metal has made me really prefer the filename versions over the moduleinfo versions...
May 14 2015
On Thursday, 14 May 2015 at 12:39:59 UTC, Adam D. Ruppe wrote:On Thursday, 14 May 2015 at 10:29:57 UTC, Kagamin wrote:AFAIK, ModuleInfo is only 8 bytes + module name.Maybe also replace file name with ModuleInfo similar to how assert works?Those bug me because all it really wants from it is the name and then you need all the moduleinfo even in bare metal.Exception support requires some RTTI anyway so maybe it doesn't matter, but my playing with bare metal has made me really prefer the filename versions over the moduleinfo versions...ModuleInfo is primarily to extract module name and use it instead of file name, you can use module name directly if you don't like full ModuleInfo.
May 14 2015
For me it feels like until the issue with non-allocating exception instances on their own is solved, everything else doesn't make that much of a difference. (I use enforce a lot)
May 19 2015