Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

throw vs assert in packages #154292

Open
Profpatsch opened this issue Jan 10, 2022 · 7 comments
Open

throw vs assert in packages #154292

Profpatsch opened this issue Jan 10, 2022 · 7 comments
Assignees

Comments

@Profpatsch
Copy link
Member

@roberth I feel like we are moving in two different directions here, and we should standardize on one of them.

assert lib.asserts.assertOneOf "firewallType" firewallType [ "iptables" "nftables" ];
assert lib.asserts.assertOneOf "dnsType" dnsType [ "internal" "systemd-resolved" ];

^ this was the way most packages wrote assertions so far, and lib.asserts was just a slight improvement over assert.

The new approach is different and diverges from the previous best practice.

Originally posted by @Profpatsch in #153740 (comment)

cc @roberth

@roberth
Copy link
Member

roberth commented Jan 10, 2022

The main reason I prefer throw over assert is the user experience, specifically error messages:

  • The message always includes a verbose source location. This will point to the reusable validation logic, rather than the function that was called with unexpected inputs. This is mostly a problem for checkable values that reside in a reusable data structure like a list or attrset.
  • The message can not be customized.

But the author also suffers:

  • It is not a first-class value. You'd have to write (x: assert foo != bar; x) if you wanted to pass it anywhere, which is probably a higher order function, so now you'd get ridiculous lambdas like seqMapAttrs (k: v: x: assert k != v; x).

And some more reasons:

  • "assertions" generally refer to internal assumptions about a program's own implementation, rather than inputs from a caller, let alone user. NixOS/module system may still move to "checks".
  • It is redundant as a language construct.
  • It encourages use of incomplete syntax in writing (docs etc)

One area where assertions are actually nice, is in simple checks, such as
assert withJpegSupport -> libjpeg != null;, because here the original expression is a useful description of the error. It does still produce a verbose source location, which does not point to the probable cause, which in this case is the caller of the package function.

My preferred direction for this issue, is to make types better suited for input validation: #154178

@sternenseemann
Copy link
Member

sternenseemann commented Jan 13, 2022

Note that nothing prevents us from having asserts and nice error messages:

assert foo || throw "my nice error message";
# …

I've been putting off converting lib.assertMsg to do something similar for a while now — I think that would already be an easy improvement.

The trick that makes throwIfNot work, return the identity function, is nice, but I fear it may be confusing to casual readers how this code (imperative looking, missing expected parentheses) even works. The assert syntax is clearer here and would be covered in an introduction to the expression language.

@Profpatsch
Copy link
Member Author

Oh, using || here is a cute trick, can we package that up into a function? Maybe even change assertMsg to use that?

@Profpatsch
Copy link
Member Author

Also refer to this nix issue: NixOS/nix#749

@roberth
Copy link
Member

roberth commented Jan 16, 2022

Oh, using || here is a cute trick, can we package that up into a function? Maybe even change assertMsg to use that?

So that would be something like

cond: msg: x: assert cond || throw msg; x

or

cond: msg: assert cond || throw msg; x: x

These are subtly different and I believe that the latter is better because it the prior will not fire until the last application, which may not occur.

The latter is behaviorally indistinguishable from throwIfNot.

Again, the assert keyword is useless when it occurs in helper functions like these.

I guess you could currently use its syntax at the call sites instead, you please, but it is semantically questionable and, like any hack, may complicate the evolution of the evaluator if, assuming nix devs take nixpkgs into account (they should).

# I wouldn't recommend this, but I think this is what you're proposing:
assert throwIfNot foo "my nice error message";
# …

I wouldn't rename throwIfNot to assertMsg, because the function itself has nothing to do with assert, making it a confusing name.

@sternenseemann
Copy link
Member

Oh, using || here is a cute trick, can we package that up into a function? Maybe even change assertMsg to use that?

As said, this has been my plan for quite some while.

I wouldn't rename throwIfNot to assertMsg, because the function itself has nothing to do with assert, making it a confusing name.

They are different things as well and changing assertMsg to return a function makes little sense, as it would break existing code.

These are subtly different and I believe that the latter is better because it the prior will not fire until the last application, which may not occur.

How so? The assert would be evaluated before the first application, if I understanding it correctly. In any case I think this is a very slim benefit and I'm still a bit concerned creating such a foreign “DSL” type thing (see #154292 (comment)).

@sternenseemann
Copy link
Member

As said, this has been my plan for quite some while.

#155400

@stale stale bot added the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Jul 31, 2022
@stale stale bot removed the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Sep 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants