Remix.run Logo
tialaramex 8 hours ago

Suppose your function doodle_widget is supposed to take a Gonzo Widget, but you're worried somebody might call it with a Non-Gonzo Widget and that can't work.

Traditionally you write code which checks the Widget to see if it's Gonzo and if not you throw an exception. Callers can pick, for this function in particular, whether to handle the Exception, in which case they get that Exception to look at, or they can "bubble it up" to be handled in their caller, and so on all the way to the top of the program where if it bubbles up it's reported and then exits the program.

With Contracts you write a contract for the function with a pre-condition that the Widget is Gonzo. Your users (programmers who might call doodle_widget) can pick: If they fail a contract the program exits immediately reporting a violation ("quick enforce"), it reports the violation via a global contract handler and then exits ("enforce") or it just reports to the handler but doesn't exit ("observe") or finally, they ignore it entirely ("ignore")

These just aren't that different. The contract is maybe slightly better because of the enhanced semantic discovery - you could imagine tooling which gives you a yellow squiggly line because your code violates a contract requirement for example, it's definitely not practical to check exception raising that way.

bluGill 8 hours ago | parent | next [-]

That is one use where either can be used.

However contracts cover a lot of other cases (and as the other replies point out contracts are probably the wrong answer here - a concept is your right answer allow someone else to write a new/different Gonzo complaint widget in the fiture). A contract can check cases where have the right type, but something is wrong anyway. If you need a sorted list a contract can check that....

Likewise exception is useful for a lot of things that should not be a contract. Running out of disk space is still a common problem - you do not want a contract that there is enough disk space, this is an error you need to ask how to handle (often the user would free up disk space external to your program and retry a save).

Almondsetat 8 hours ago | parent | prev | next [-]

>Suppose your function doodle_widget is supposed to take a Gonzo Widget, but you're worried somebody might call it with a Non-Gonzo Widget and that can't work.

Can't you just use concepts?

tialaramex 8 hours ago | parent | next [-]

No. Bjarne's C++ 20 Concepts are basically duck typing, you can express that you want a type where we can call the "is_gonzo" function but you can't say that you only want values of that type in which it's true.

Obviously you could re-design the software to follow a Rust-style type-state paradigm, have a NonGonzoWidget and GonzoWidget type which both inherit from Widget and now you can have your function take a GonzoWidget - but that's not what my comparison was about and this technique while possible is less common in C++

Edited to add: Actually, I should clarify that concepts have an idea called "modelling" and you can model anything you want, but the problem is that the machine doesn't care. So you can have a concept which models strings being about how awesome Donald Trump is, and the compiler won't and can't check that, but now a C++ program with a string matching that concept and the value "Trump is a moron" is an invalid C++ program, still compiles, still probably works, but your concept wasn't "modelled" so the program was not valid for whatever that's worth....

jandrewrogers 7 hours ago | parent | prev [-]

Yes, these types of scenarios are commonly handled with C++20 concepts. The main thing contracts do is provide a standardized scaffolding for enforcement.

jayd16 8 hours ago | parent | prev [-]

How do you handle I/O type exceptions with contracts?

jsmith45 7 hours ago | parent | next [-]

You wouldn't.

But keep in mind that there are multiple kinds of exceptions.

Some exceptions, indicate scenarios the caller could reasonably have avoided. This include things like argument bound or null checks. A caller absolutely could avoid these exceptions by doing its own checks. This can be split between checks that the caller really should have done, like not passing null to a function that cannot take null, vs those where baking the relevant knowledge into the caller would be undesirable (perhaps because future versions of the library expect to accept more values).

There are exceptions that in theory a caller could avoid, but in practice it is impractical, like an exception thrown by a parser of some complex format if the provided input isn't legal. The only good way to avoid such an exception is to have a non-throwing parser that you can check with, but you probably don't want to parse twice. An alternative interface for the parser might be able to totally avoid the exception, returning either succeeded (with result tree) or fail with error message, but that would be a design choice of the implementor, not the caller.

There can be exceptions that indicate a logic bug in the impleentation (things like throwing if some invariant the implementation is in charge of is violated), but more often this is assertions instead.

Lastly, you have exceptions that there is no possible way the caller could always avoid. IO exceptions are among these. While you can sometimes do existence, space, or permission checks or similar to reduce the probability of getting certain exceptions, something else could race your app between the check and performing IO, and make them happen anyway.

The primary target for contracts are exceptions that callers both could and should have avoided, and the exception/assertion case that is trying to verify the implementation is working as expected.

The other categories of exceptions are more or less totally out of scope.

tialaramex 8 hours ago | parent | prev [-]

You would absolutely be entitled to write a contract which says there are never I/O problems. It could even make sense in some cases, though often not.