Live data from Hacker News

Rust's Two Kinds of 'Assert' Make for Better Code

tratt.net

41–50 of 87 posts

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#41
It's worth pointing out that Python's asserts can also be "compiled" away if you use the -O flag (or PYTHONOPTIMIZE=1), which eliminates their runtime cost.

It's also worth pointing out that this is the reason you should *never* put side-effects or security-relevant checks in assert statements. For example, you should never do something like this:

  assert f.read(4) == b"\x89PNG", "Not a PNG file"
  # proceed to read and parse the rest of the file
but rather, you should do

  magic = f.read(4)
  assert magic == b"\x89PNG", "Not a PNG file"
so that your code doesn't suddenly break when someone decides to be clever and use -O.

Also, fun unrelated fact: Python does have something like a preprocessor, although it's rarely used. If you condition on the flag __debug__:

  if __debug__:
    expensive_runtime_check()
and then run Python with -O, the if statement and its body will be entirely deleted from the bytecode - even the `if` check will be deleted. It can be used for including "debug" code in hot code, where the extra flag check itself might be expensive.

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#43

Nim has this too: - `assert` is disabled in unsafe `danger` mode or can be disabled with a flag for performance - `doAssert` cannot be disabled While I've never come across an argument for why there are two types of assert, over time I’ve naturally started using them in the same way as the author.

How are people still making the classic `foo`/`foo_safe_version` mistake? In a supposedly modern language. And they didn't even name the safe version clearly! If the rest of Nim is designed on this level then I hope it never succeeds. Note that Rust got this exactly right. assert, debug_assert. Clear and fail-safe.

I don't understand, what's wrong with assert and doAssert?

They are both enabled in release AND debug modes. You would have to explicitly compile code with -d:danger flag to disable any assertions.

> And they didn't even name the safe version clearly!

In this context safe version is clearly named as "release" mode, and unsafe one is even more clear - "danger" mode. "danger" obviously implies it should be used with caution.

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#44

Earlier quoted context omitted.

In general, you need a type system that supports sets of integer values, e.g. range(2, 7) or set(3, 5, 7). Rust doesn't support that unfortunately so it has a special annotation instead to make NonZero work.

In terms of category theory, what we would need is subtraction and division types on top of product and sum types. So a 32-bit integer is the product of 32 two-state bit types. Something akin to NonZero could be defined as that type minus one state, such that there are now 4294967296 - 1 representable values. Similarly, pointer types on some machines always have some bits set to 0 due to hardware constraints. These c…

Ada has constrained range types. Idris has lots more fun stuff with types. But not very mainstream yet.

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#45
post #17

"Using runtime assert() in a tight performance loop can impact performance?" Um, like ... duh? Run time checks should simply be enabled. Normally, you're checking some context on entry or exit from a function. 99% of the time it simply won't matter to performance. And, when it does, it will pop out at you and you can remove it. The bigger issue as alluded to is assert() in libraries. As a user, you can't add an asser…

Yeah, assert in a performance loop is generally a mistake.

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#46

It's worth pointing out that Python's asserts can also be "compiled" away if you use the -O flag (or PYTHONOPTIMIZE=1), which eliminates their runtime cost. It's also worth pointing out that this is the reason you should *never* put side-effects or security-relevant checks in assert statements. For example, you should never do something like this: assert f.read(4) == b"\x89PNG", "Not a PNG file" # proceed to read and…

> but rather, you should do > magic = f.read(4) > assert magic == b"\x89PNG", "Not a PNG file"

If the `assert` compiles out, wouldn’t -O also possibly compile the `read()` out as well given `magic` isn’t used after the assign?

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#47
post #37

I don't think I've ever in 30+ years of programming seen a runtime assertion that wouldn't be improved by doing something else instead. Writing unit tests and choosing designs that codify the constraints in the type system are two things that are obvious. The cost of a failing assertion is often incurred at some point where it is most expensive. I know this because I've worked for companies that have charged exorbita…

Respectfully I completely disagree and I think you've got it rather backwards since you've had some bad experiences (understandable).

Assert and (unit)tests are completely orthogonal and unrelated things in terms of functionality even though both are aimed at improving the software correctness.

Failing an assert in production of course sucks and is costly. But what is more costly is letting the bug slip through and cause hard to diagnose bugs, program incorrectness and even (in some cases) silent address space corruption that will then manifest itself in all kinds of weird issues later on during the program run.

The whole point of (correct) use of asserts is to help make sure that the program stays within its own well defined logic and doesn't veer off course and if it does then make that bug immediately as loud as clear as possible.

When the bugs are quick to detect and diagnose you'll learn that you have less and less asserts triggering in production and thus you end up with improved product quality.

As a general rule I'd say use asserts liberally to verify things such as invariants, post- and pre-conditions and ALWAYS have them built-in.

Finally I want to point out that using assert is not ERROR checking (file not found, IP address not resolved, TCP connection failed, system resource failed to allocate, etc.) but BUG checking.

Do not write code for BUGS.

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#48
post #36

What about the fact that the assert causes a panic that crashes the program? Is there not a better alternative of doing your bounds checking as a first class aspect of the function and returning an error? If someone is deep into a session with your application and the cursor somehow drifts into an unknown state (which could be corrected with the “home” key or something else), for example, getting too crazy with asser…

Your example is an incorrect use of an assert.

You never use assert for conditions that are logical (error) conditions that the program is expected to handle.

For example your browser handling 404 HTML error is only an error from the user perspective. From the software correctness perspective there's no error, there's just a logical condition that needs to be reasoned about.

Compare this to a BUG which is a mistake (an error made by the programmer), for example violating some invariant, going out of bounds on an array etc.

This is a scenario where the program is violating its own logic and constraints and as a result is no longer in a valid state. Assert is a tool to catch BUGS made by the programmer and nothing else.

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#49

It's worth pointing out that Python's asserts can also be "compiled" away if you use the -O flag (or PYTHONOPTIMIZE=1), which eliminates their runtime cost. It's also worth pointing out that this is the reason you should *never* put side-effects or security-relevant checks in assert statements. For example, you should never do something like this: assert f.read(4) == b"\x89PNG", "Not a PNG file" # proceed to read and…

> but rather, you should do > magic = f.read(4) > assert magic == b"\x89PNG", "Not a PNG file" If the `assert` compiles out, wouldn’t -O also possibly compile the `read()` out as well given `magic` isn’t used after the assign?

No. Ignoring that Python does not have anywhere near this level of optimisation, read() has side effects so optimising it away would be broken in the general case.

It could be optimised away if all following uses would invalidate (seek, but only with SEEK_SET or SEEK_END) or ignore (pread/pwrite) the file offset, but that seems like an enormous amount of fussy work for what I would guess is little to no payback.

Re: Rust's Two Kinds of 'Assert' Make for Better Code

#50

Earlier quoted context omitted.

In general, you need a type system that supports sets of integer values, e.g. range(2, 7) or set(3, 5, 7). Rust doesn't support that unfortunately so it has a special annotation instead to make NonZero work.

In terms of category theory, what we would need is subtraction and division types on top of product and sum types. So a 32-bit integer is the product of 32 two-state bit types. Something akin to NonZero could be defined as that type minus one state, such that there are now 4294967296 - 1 representable values. Similarly, pointer types on some machines always have some bits set to 0 due to hardware constraints. These c…

As far as I understand it there's no need for that. You don't need to say [0, 256) except 0; you can just say [1, 256).
Post reply on HN