See more at the announce forum.
I was on chat again (what a surprise lol) discussing my position that strings are bad, and you should put information in variables instead. A person countered that this requires making new types, and making new types is a hassle.
Well, creating new types is actually pretty easy in D: we often do it without even thinking about it, with the return value of implicitly instantiated templates, like with the std.algorithm pipelines.
I made an experimental module, arsd.exception, a while ago to demonstrate some of this. My big focus there was improving on enforce and the module isn't entirely usable, just a concept demo (yes, a bit silly to give it the full arsd treatment then, but meh). I'm now looking at just making new exceptions with data attached.
Let me show you the potential usage:
import exception2; enum MyError; enum OtherError; void main() { int a; try { throw Exception2!MyError(a, "more info"); } catch(Exception2!(OtherError) e) { // won't catch this, since it is another error import std.stdio; writeln("wrong"); } catch(Exception2!(MyError, int) e) { // will catch this though! import std.stdio; writeln("CAUGHT!"); writeln(e); } }
You might notice that I threw both an int and a string, but caught only the int. The idea here is all the details you add are just further specializations, you can process them or not, depending how much of the data you're interested in. You can just catch(Exception2!MyError) if you don't care about the attached data too.
Of course, another option here would be to statically list the data in a struct MyError instead of enum MyError. On the other hand, you could throw away all the namespaced type differentiation and just use Exception2!("some string", data):
import exception2; void main() { int a; try { // string error type instead of namespaced D type // can still attach information though throw Exception2!"foo bar"(a); } catch(Exception2!"foo bar") { // caught by string import std.stdio; writeln("caught"); } }
I don't like this much myself, since you might use the same string as a different library, and they would be no way to distinguish them. With an enum, you can differentiate through module import and namespace rules.
But the advantage here is that there's nothing declared ahead of time at all - the new exception class is declared right at the throw point, while still being able to be used again at the catch point to identify it.
While I don't love this style, it is at least as convenient (often more!) as the common use of throw new Exception("some string"), and still gives benefits of easy-to-attach data.
Now, you might argue that different data attachments should always be a different exception family anyway, which would lean toward just declaring a new class anyway, which can be quite easy to do with a mixin template giving you some of the boilerplate. So if you're gonna declare a struct, or even an enum, you might as well declare the associated data there and this whole scheme doesn't gain much.
However, I still think the enum strikes a good middle ground: you get to declare a static list of exception families, then add data as needed. Consider a InvalidValue family: the attached info might give int valueGiven, int maxValue or it might give int valueGiven, int minValue, int maxValue. Or maybe string valueGiven, string expectedPattern. So one family might reasonably have different values attached.
Of course, this example also gives a reason why you might want some kind of structured data: if you just got two int values, which one was valueGiven and which maxValue? Or were two values given (e.g. a matrix coordinate)? But, even using this scheme, you can still throw Exception2!InvalidValue(MyStruct(structured, information, here);, and people can either catch(Exception2!InvalidValue) or catch(Exception2!(InvalidValue, MyStruct)) and capture the family or the details. So it'd still work, just the argument that you need to declare struct MyStruct {} vs class InvalidValueExceptionWithMyStruct : InvalidValueException { MyStruct data; mixin ExceptionCtor;s } isn't quite as compelling.
...just quite frankly, the status quo of D is we rarely see any of this. We most often just see throw new Exception("invalid value"); - with no type to catch and often, no information about the value at all either. Any of my Exception2 schemes are better than the typical Exception we see in practice today.
People in the chat when I was first showing it saw the throw Exception2... and assumed there was something to do with @nogc in here. While perhaps you could, that's a separate topic that I don't want to talk about again today. No, my implementation still uses new at some point, so why is it not at the usage site?
It is actually because I want to use that IFTI - implicit function template instantiation - to attach data, which means a new subclass needs to be created. You can't do that with a normal constructor call. But you can do it with an opCall!
Moreover, I also wanted to make sure the derived class could be named outside the declaration point, so people can easily catch the exceptions. This rules out anonymous classes (ok, not exactly ruled out, you could make it work, but it gets messier), but still worked with the opCall.
Let's dig into the code:
1 module exception2; 2 3 /+ 4 It uses the long-form template writeup because I want to define the Parent separately 5 and this just makes it a bit easier. 6 +/ 7 template Exception2(alias Type, T...) { 8 // Each piece of added data is a specialization of the more general 9 // thing. This is realized by making the parent class just be the same 10 // thing but with one piece chopped off. 11 // 12 // Or fallback to Exception as the generic parent of all Exception2s. 13 static if(T.length) 14 alias Parent = Exception2!(Type, T[0 .. $-1]); 15 else 16 alias Parent = Exception; 17 18 class Exception2 : Parent { 19 // this should probably be named differently or at least const or something 20 // but it holds the data passed at the throw point 21 T t; 22 23 // This is the main entry point for throwing - notice how it takes the 24 // arguments and forwards them to a new class - just like the factory pattern 25 // often used in Phobos to construct, e.g., a MapResult from a map(). 26 // 27 // If you tried to use `new Exception2` directly, you'd have to specify all those 28 // types which you are about to pass, but with the opCall, the `R` is implicitly inferred. 29 // 30 // Note the inferred return value is the full static type passed down. 31 static opCall(R...)(R r, string file = __FILE__, size_t line = __LINE__) { 32 return new Exception2!(Type, T, R)(r, "", file, line); // that string could hold something i guess 33 } 34 35 // you wouldn't call this directly! I might even be able to make it `protected` 36 this(T t, string msg, string file = __FILE__, size_t line = __LINE__) { 37 this.t = t; 38 static if(is(Parent == Exception)) 39 super(msg, file, line); 40 else 41 super(t[0 .. $-1], msg, file, line); 42 } 43 44 // this is basically the same as the old arsd.exception 45 //nothing too special here 46 override void toString(scope void delegate(in char[]) sink) const { 47 48 import std.conv; 49 50 sink(typeid(this).name); // FIXME: eponymous things so long of a name 51 52 sink("@"); 53 54 sink(file); 55 sink(":"); 56 sink(to!string(line)); 57 58 sink("\n"); 59 60 // because I love this phrasing lol 61 // just a joke though 62 sink("The program has performed an illegal operation and will be shut down."); 63 64 // this part is real though: when printing, loop through the attached data 65 // and show them all 66 foreach(idx, item; t) { 67 sink("\n"); 68 sink(typeof((cast() this).t[idx]).stringof); 69 sink(" = "); 70 sink(to!string(item)); 71 } 72 73 if(info) { 74 try { 75 sink("\n----------------"); 76 foreach (t; info) { 77 sink("\n"); sink(t); 78 } 79 } 80 catch (Throwable) { 81 // ignore more errors 82 } 83 } 84 } 85 } 86 }
This is the flexible class that lets you both define catch(Exception2!X) as well as throw Exception2!X(data) - the basic thing declares an opCall which returns a further derived object, constructed for you. Not every day you see static opCall on a class, but it is a pattern I've used before (see my old entry on declarative guis in D for another use) and it works quite well.
I'm pretty happy with this result. You get the convenience of throw new Exception("stuff") with far more structure, without giving you as much hassle in declaring it. I want to pursue this further; perhaps starting to use it in some way in the arsd libs.
DConf is this week, so nothing to say about it until after it happens (though I'll be doing some comments on the chat rooms, likely after the fact given the timezone shift and other business - i still have to work!), but i'll show an experimental exception concept I've been playing with again.