The assert is very handy statement, but when it fails there's no info why it failed.
let a = 1
let b = 2
assert a == b
Error:
Error: unhandled exception: /usercode/in.nim(4, 8) `a == b` [AssertionDefect]
And when you see such assert error in console, you usually need to do:
Lot of boring work. Would be better if assert would also tell us why it failed.
For example, if assert is used for equality test assert a == b and if string representations of $a and $b are short - print it along with the error.
Or, if it's not desirable for the assert default behavior, allow to extend it with assert_hook, something like when compile(assert_hook(a, b)): assert_hook(a, b).
You can use unittest.check for a more descriptive message.
import std/unittest
let
a = 1
b = 2
check a == b
Outputs:
/usercode/in.nim(5, 8): Check failed: a == b
a was 1
b was 2
It is possible to add a message to assert:
let a = 1
let b = 2
assert a == b, "Something didn't work."
And we get:
Error: unhandled exception: /tmp/test.nim(4, 8) `a == b` Something didn't work. [AssertionDefect]
You may put anything in the message, for instance the value of the arguments:
import strformat
let a = 1
let b = 2
assert a == b, &"Expected {b}, got {a}"
Of course, displaying the arguments by default would be an improvement but, most of the time, only the user knows what is the relevant information to display.
You may put anything in the message
Yes. The whole point of this post is to avoid doing it :)
Yes, thanks for the implementation, but I was hoping to convince to improve the default assert implementation.
I already rewritten pretty much the whole Nim std library, as the Nim std is mostly low-level C-like API and is not convenient for high-level app development or data processing, like python/ruby etc.
This is the main reason I use Nim, as I can change almost anything in the language and turn Nim into typed Python/Ruby.
But still, it's good when the language by default does what you want :)
If it's server, you need to re-create all the condition to hit it.`
This means what you actually need is a real logger, not assert.
assert is not a logger and cannot replace a logger. A nice example of logger is chronicles
Yes, reproducing production environment (or complex stateful environment) is unrealistic. There are many situations that are resource-consuming or impossible to simulate. That is the reason why a logging system is required in production. assert in general may not even has effect in production (though Nim only turn off assert in danger mode).
And if it's in logs, you can't re-create it, and had to guess what's wrong with a or b.
Speaking from my experience, it usually not enough to merely know the value of two variables and then be able to debug. There is usually a much larger context in order to know why it went wrong. Since you have mentioned a server, which I assume web server, a robust logger system should start from logging session ID for each request and then record the branching history and accumulate reasonably-sized variables and the return status. Steam the log (in json usually) at the end of request or on error to monitoring system and search engine (e.g. ELK).
I somehow agree that this is boring work, but this is valuable to quickly respond to incident and trace back only-production-seen bugs.
Or, if it's not desirable for the assert default behavior, allow to extend it with assert_hook, something like when compile(assert_hook(a, b)): assert_hook(a, b)
IMO, please don't do that. If someone want a very very powerful assertion, put that in nimble.
The main difficulty in supporting assert to behave like unittest.require is how to avoid cyclic dependencies. assert is used in low-level modules, but implementing it to dump variables on failure would require either importing std/macros (causing cyclic deps) or using a few select magics from std/macros to implement this.
A 2nd tricky part is avoiding multiple evaluation in some edge cases, eg: assert lhs() == rhs() => to avoid multiple evaluation you need: let a = lhs(); let b = rhs(); ... but that can cause un-necessary copies depending on typeof(a); and using let a = lhs().unsafeAddr doesn't always work. But unittest.require faces the same problem.
Definitely doable and worth doing though.
Something you could do is take the expression given to the macro, make temps for each parameter in each call inside it, then print what they were, similar to Groovy's power assert. Then you can get something like:
powerAssert foo(a, bar(b, c)) == d * e
# assert failed:
# a was ...
# b was ...
# c was ...
# bar(b, c) was ...
# foo(a, bar(b, c)) was ...
# d was ...
# e was ...
# d * e was ...
FWIW, I think the let a=..; let b=.. is fine if even if it introduces copies. Optimizing seems out of scope for asserts - you really optimize by just not running them. :-) As @jackhftang correctly observed, asserts are not a replacement for logging.
Not sure a more capable binOpAssert or powAssert are "too much" for the stdlib. I like big tent/all of the above stdlibs, but priorities vary widely and we've had a hard time growing Nim's stdlib.
As a reader, I would prefer (for powAssert) an output format like "condition that failed" then "subexpression components"..basically failure then explanation outside-in..basically the reverse of @Hlaaftana's - that way I can maybe stop reading earlier. (I know some people read errors bottom-up...Just registering an opinion).