Jump to page: 1 2 3
Thread overview
opDispatch, duck typing, and error messages
Apr 21, 2011
Adam D. Ruppe
Apr 21, 2011
bearophile
Apr 21, 2011
Jonathan M Davis
Apr 21, 2011
Adam D. Ruppe
Apr 21, 2011
Jonathan M Davis
Apr 21, 2011
Andrej Mitrovic
Apr 21, 2011
Andrej Mitrovic
Apr 21, 2011
Adam D. Ruppe
Apr 21, 2011
Jonathan M Davis
Apr 21, 2011
Adam D. Ruppe
Apr 21, 2011
Jonathan M Davis
Apr 22, 2011
Adam D. Ruppe
Apr 23, 2011
Jacob Carlborg
Apr 23, 2011
Jonathan M Davis
Apr 24, 2011
Jacob Carlborg
Apr 22, 2011
spir
Apr 21, 2011
Sean Kelly
Apr 22, 2011
Andrej Mitrovic
Apr 22, 2011
Robert Jacques
Apr 22, 2011
spir
Apr 22, 2011
spir
April 21, 2011
I just made an innocent little change to one of my programs, hit compile, and got this vomit:

/home/me/d/dmd2/linux/bin/../../src/phobos/std/conv.d(97): Error: template
std.conv.toImpl(T,S) if (!implicitlyConverts!(S,
T) && isSomeString!(T) && isInputRange!(Unqual!(S)) &&
isSomeChar!(ElementType!(S))) toImpl(T,S) if (!implicitlyConverts!(S
,T) && isSomeString!(T) && isInputRange!(Unqual!(S)) &&
isSomeChar!(ElementType!(S))) matches more than one template declar
ation, /home/me/d/dmd2/linux/bin/../../src/phobos/std/conv.d(185):toImpl(T,S)
if (isSomeString!(T) && !isSomeChar!(ElementT
ype!(S)) && (isInputRange!(S) || isInputRange!(Unqual!(S)))) and
/home/me/d/dmd2/linux/bin/../../src/phobos/std/conv.d(289)
:toImpl(T,S) if (is(S : Object) && isSomeString!(T))



Whooooo... took a bit to figure out what it was saying. The bottom line: one of my classes matched both Object and isInputRange because it offers an unrestricted opDispatch.

The fix:

// note the constraint
string opDispatch(string name)() if(name != "popFront") {}


(I'm sure empty or front would have worked just as well, but popFront I'm sure I didn't actually use anywhere for this.)


This post is to serve as two things: an FYI in case you see something like this yourself, and to have a discussion on what we can do to improve the situation.

A constraint like this wouldn't work of the Object was actually supposed to be a range.


So, what can we do to improve that message? As it is now, it's
close to useless. Yeah, I was able to track down what it meant, but
only after I hacked up my copy of Phobos to tell me what T and S
actually were in the instantiation of to... then, it was obvious what
the error meant, but before, well, I pity the poor newbie who
stumbles upon that!


Things I think would help:

a) If the compiler gave some kind of stack trace in this instance. An
error message pointing solely at std.conv doesn't help much. In a
lot of template error messages, the kind of trace I want
already exists, so I suspect 90% of the work to implement it is
already done.

b) Format those constraints a little. Just put a "\n\t" before the if and a "\n" before the function name. I think some whitespace would help a lot in readability. Sure, newbs might still not get it, but at least it won't be a blob of wtf.

Alas, I've looked at the compiler, but aren't to the point where I can contribute code to it myself. Well, maybe I could do the formatting change, but I haven't tried yet.

Regardless, let's look at other options.


c) Maybe Phobos could help out somehow? Another thing to!()
annoys the living crap of me with is it's runtime errors. It, again,
doesn't tell me where in my code the problem occurred.

Perhaps have it take default __FILE__ and __LINE__ args to print out too? I think this can help both compile and runtime errors.

d) Also in Phobos, I wonder if we can beautify the message somehow. I don't have any idea how to do this in a scalable way.

It could static if (matches constraint 1 and constraint 2)
static assert("pretty message"), but there's no chance that would
scale well.

So I don't know here.


Or, there's a whole new approach:

e) Duck typing for ranges in to!() might be a bad idea. Again, remember,
a class might legitimately offer a range interface, so it would
trigger this message without opDispatch.

If ranges are meant to be structs, maybe isInputRange should check is(T == struct)? This doesn't sit right with me though. The real problem is to!() - other range functions probably don't overload on classes separately than ranges, so it won't matter there.


I think the best thing to do is simply to prefer Object over range.

toImpl(T) if (isInputRange!(T) && (!is(T : Object)))

Or something along those lines. Why? If the object has it's own toString/writeTo methods, it seems fairly obvious to me anyway that to!string ought to simply call them, regardless or what else there is.



I kinda blabbered here, but in the end, I think my previous paragraph is the big thing. It's a fairly minor Phobos change. Any objections to it?

Note btw that I'd still like the error messages to be prettier, but I'm ok doing it one step at a time.
April 21, 2011
Adam D. Ruppe:

> c) Maybe Phobos could help out somehow? Another thing to!()
> annoys the living crap of me with is it's runtime errors. It, again,
> doesn't tell me where in my code the problem occurred.
> 
> Perhaps have it take default __FILE__ and __LINE__ args to print out too? I think this can help both compile and runtime errors.

This is a more general problem of runtime errors, not just of to!(). Maybe exceptions nature should be changed a little so they store __FILE__ and __LINE__ on default (exceptions without this information are kept on request, for optimization purposes).

Bye,
bearophile
April 21, 2011
> Adam D. Ruppe:
> > c) Maybe Phobos could help out somehow? Another thing to!()
> > annoys the living crap of me with is it's runtime errors. It, again,
> > doesn't tell me where in my code the problem occurred.
> > 
> > Perhaps have it take default __FILE__ and __LINE__ args to print out too? I think this can help both compile and runtime errors.
> 
> This is a more general problem of runtime errors, not just of to!(). Maybe exceptions nature should be changed a little so they store __FILE__ and __LINE__ on default (exceptions without this information are kept on request, for optimization purposes).

Most exceptions end up with file and line numbers. The problem is generally not that they don't have a file or line number, it's that the file and line number is from inside of a function that you called instead of your own code. As long as you have a stack trace, it's not all that big a problem, but if you're on Windows, then there are no stack traces yet and you're screwed. It _does_ generally make sense for the file and line number to be from the throw point rather than the point where you called the function (especially when the throw point could be several function calls away in the stack), but without a proper stack trace, you have no way of knowing where in your code the problem is.

Now, in the case of something like to, the types are known at compile time, so in many cases, it should be able to give a compile time error via a template constraint, but it's not able to do that in all cases. One example of that is converting a string to anything else. The value of the string determines whether the conversion is valid rather than the types being enough at compile time. So, in some cases, you _have_ to have an exception at runtime rather than a compile time error. Whether to does as good a job with making errors compile time errors as much as it can, I don't know, but on some level, we're stuck.

But generally, I think that the real problem is the lack of a stack trace. You generally get them on Linux but not Windows. Most exceptions _should_ be grabbing the file and line number that they're thrown from. I don't recall of Exception can or not (Error _can't_ due to some low level stuff), but pretty much everything derived from Exception certainly can and should.

- Jonathan M Davis
April 21, 2011
> This is a more general problem of runtime errors, not just of to!(). Maybe exceptions nature should be changed a little so they store __FILE__ and __LINE__ on default (exceptions without this information are kept on request, for optimization purposes).

Vote up for that. At least it should do that in -debug mode. It's completely useless getting an error message with a file and line number for an internal Phobos function, like some funcImpl() private function, when the fault is a runtime argument.
April 21, 2011
Btw, there is a stack trace for Windows and D2, see here: http://3d.benjamin-thaut.de/?p=15
April 21, 2011
bearophile wrote:
>  Maybe exceptions nature should be changed a little so they store
> __FILE__ and __LINE__ on default (exceptions without this
> information are kept on request, for optimization purposes).

I'd like that. Even with a stack trace, it's nice to have that available right at the top.

A while ago, someone posted a stack tracer printer for Linux to the newsgroup. Using that, this program:

void main() {
	throw new Exception("test");
}

 dmd test60 -debug -g backtrace.d

Prints:

object.Exception: test
----------------
./test60(_Dmain+0x30) [0x807a5e8]
./test60(extern (C) int rt.dmain2.main(int, char**) . void runMain()+0x1a) [0x807d566]
./test60(extern (C) int rt.dmain2.main(int, char**) . void tryExec(void
delegate())+0x24) [0x807d4c0]
./test60(extern (C) int rt.dmain2.main(int, char**) . void runAll()+0x32) [0x807d5aa]
./test60(extern (C) int rt.dmain2.main(int, char**) . void tryExec(void
delegate())+0x24) [0x807d4c0]
./test60(main+0x96) [0x807d466]
/lib/libc.so.6(__libc_start_main+0xe6) [0xf75a5b86]
./test60() [0x807a4e1]



No line or file info! I'd really like to have something there. Though, actually, whether it's in the message or in the stack trace doesn't really matter. As long as it's there somewhere.

Most my custom exceptions use default params in their constructor to add it. Perhaps the base Exception should too?
April 21, 2011
On Apr 21, 2011, at 4:10 PM, Andrej Mitrovic wrote:

> Btw, there is a stack trace for Windows and D2, see here: http://3d.benjamin-thaut.de/?p=15

Already in the next DMD release.
April 21, 2011
> bearophile wrote:
> > Maybe exceptions nature should be changed a little so they store
> > 
> > __FILE__ and __LINE__ on default (exceptions without this information are kept on request, for optimization purposes).
> 
> I'd like that. Even with a stack trace, it's nice to have that available right at the top.
> 
> A while ago, someone posted a stack tracer printer for Linux to the newsgroup. Using that, this program:
> 
> void main() {
> throw new Exception("test");
> }
> 
> dmd test60 -debug -g backtrace.d
> 
> Prints:
> 
> object.Exception: test
> ----------------
> ./test60(_Dmain+0x30) [0x807a5e8]
> ./test60(extern (C) int rt.dmain2.main(int, char**) . void runMain()+0x1a)
> [0x807d566] ./test60(extern (C) int rt.dmain2.main(int, char**) . void
> tryExec(void delegate())+0x24) [0x807d4c0]
> ./test60(extern (C) int rt.dmain2.main(int, char**) . void runAll()+0x32)
> [0x807d5aa] ./test60(extern (C) int rt.dmain2.main(int, char**) . void
> tryExec(void delegate())+0x24) [0x807d4c0]
> ./test60(main+0x96) [0x807d466]
> /lib/libc.so.6(__libc_start_main+0xe6) [0xf75a5b86]
> ./test60() [0x807a4e1]
> 
> 
> 
> No line or file info! I'd really like to have something there. Though, actually, whether it's in the message or in the stack trace doesn't really matter. As long as it's there somewhere.
> 
> Most my custom exceptions use default params in their constructor to add it. Perhaps the base Exception should too?

I just checked. Exception _does_ take a default file and line number. So, anything derived from Exception doesn't have a file and line number, something is amiss.

- Jonathan M Davis
April 21, 2011
Jonathan M Davis wrote:
> It _does_ generally make sense for the file and line number to be from the throw point rather than the point where you called the function (especially when the throw point could be several function calls away in the stack), but without a proper stack trace, you have no way of knowing where in your code the problem is.

I believe I agree completely.

> Now, in the case of something like to, the types are known at compile time, so in many cases, it should be able to give a compile time error via a template constraint, but it's not able to do that in all cases.

Indeed. And it usually does. The problem is that the error can be pretty hard to read.

Here's one simple case:

===

import std.conv;

struct Test {}

void main() {
	Test t;
	int a = to!int(t);
}

===

/home/me/d/dmd2/linux/bin/../../src/phobos/std/conv.d(95): Error: template
std.conv.toImpl(T,S) if (!implicitlyConverts!(S,T) && isSomeString!(T) &&
isInputRange!(Unqual!(S)) && isSomeChar!(ElementType!(S))) does not match any
function template declaration
/home/me/d/dmd2/linux/bin/../../src/phobos/std/conv.d(95): Error: template
std.conv.toImpl(T,S) if (!implicitlyConverts!(S,T) && isSomeString!(T) &&
isInputRange!(Unqual!(S)) && isSomeChar!(ElementType!(S))) cannot deduce template
function from argument types !(int)(Test)
/home/me/d/dmd2/linux/bin/../../src/phobos/std/conv.d(95): Error: template
instance errors instantiating template
/home/me/d/dmd2/linux/bin/../../src/phobos/std/conv.d(7): Error: template instance
std.conv.to!(int).to!(Test) error instantiating


Holy crap!

But, if you know the secret there - to look at the last line - it does tell you enough to do some useful stuff. Alas, it doesn't tell where in *your* code the problem is, but it does tell the types you asked for, so it's pretty useful.

Still, I'd like it if that was formatted nicer, and it went the final step of telling me where in my code the problem is too.


Then, you have the problem if your type matches two templates. Then you have the vomit in my opening post, where it doesn't even tell you what types it's having trouble with!

> One example of that is converting a string to anything else. The value of the string determines whether the conversion is valid rather than the types being enough at compile time.

The runtime error is actually pretty good. If it had a nice stack trace with line numbers I'd call it outright excellent.


So I agree with your conclusion too about the stack trace!


BTW, while I'm talking about this, is RangeError at all possible to include the actual key that was out of range?

	int[] b;
	int c = b[3];
core.exception.RangeError@test60(7): Range violation

Good, but I'd call it great if it could say Range violation (3) or
something like that.

I imagine since it's an Error there might be memory allocation restrictions... but if it's possible, that'd be way cool too.
April 21, 2011
Jonathan M Davis wrote:
> I just checked. Exception _does_ take a default file and line number.

Huh, maybe my dmd is getting old.

Maybe we should revisit this after the next dmd release. Sounds like one is coming pretty soon with a lot of yummy goodness in it. All of this Exception stuff may be moot.
« First   ‹ Prev
1 2 3