Are const functions pure?

By 'pure' I mean free of side-effects.

I get the feeling that this isn't the case but can anyone provide an example of an unpure const function.

Does the compile time side effect matter? If so, const function can trigger compile error thus unpure. If you only care about runtime side effect, const function should be able to run without runtime so it can't touch the runtime itself besides the computation itself.

Suggested reading: Thoughts on Compile-Time Function Evaluation and Type Systems (by Ralf Jung)

Is crashing at runtime a side effect for you?

I think it is.

Here is one: Rust Playground

Crashes in release mode too.

(const fns are not guaranteed to be evaluated at compile time, they CAN be, but that doesn't mean they WILL be).

3 Likes

It's debatable whether functions that don't return normally are pure or not. At least some people think they are.

I'm interested in 'correct' code so I'd like to know if it's possible to have an unpure const function disregarding panicking or infinite loops.

Having read the post, I had to respond definitively to that case in point: inputing something that generates an error does not make the function impure. As long as it does so consistently, we’re good to go... be pure:))

When the compiler “sometimes” generates a compile time error, if construed to mean “randomly”, then impure.

The definition is not really up for debate. The mathematical definition of a valid function is sound and complete; a valid function is aka pure.

The compile time evaluation question likely hinges on “every time with these same inputs?”.

It depends on what you consider pure.

Const fn are (will be) allowed to take &mut. If your definition of pure allows for this, by making the state of the referee part of the input and output, const fn can be pure. If it doesn't, they're not.

The most important property of const in Rust is that it doesn't matter when you run it, compile time or runtime, you get the same result. If you give it the same input with the same state accessible to the function, it has the same result on the state.

To me this sounds like const functions are pure. And so are many "pure" transformations implemented with nonpure operations.

Maybe nothing is pure, because it has the side effect of warming your CPU.

I don't really think "is this pure" is a useful question, honestly. The useful question is what side effects do you ignore (as they are transient or otherwise don't matter and are excluded in your model), what is input domain (including any visible state), and what is your output range (including any modified state). Once you've identified those, you can ask if a function is pure, but at that point it is, because you've identified all of the classically impure inputs and outputs.

As the saying goes (roughly):

The beginner has side effects everywhere, because they know nothing else.
The intermediate have few side effects, because they've learned to avoid them.
The experienced have a reasonable helping of controlled side effects, because they want to get work done.
The expert has no side effects, because they account for them in the model.

1 Like

I agree this is a useful distinction despite the fact that passing a ref around is not pure because the ref itself does not represent the effect. I only make the distinction to clarify what we mean by pure, but less useful in the bigger picture.

Allowing for that technicality, in my experience it often boils down to a stylistic choice. When I’m designing around this type of decision I find there is value in calling this out. Of course the user can quickly see where a chain of method calls is the intended use by definition of the return value, but I’ll forward that there is nothing wrong with articulating the intent. For instance, when a status is returned, be clear whether and what the side-effect was. The docs generally do a solid job here. It helps.

All in all, by definition of the impl block of your type, or the impl of a trait where each function has access to self, pure or not becomes mostly a stylistic choice.

There a couple of design approaches I have yet to figure out in Rust:

  1. The importance of stand-alone, “essentially” side-effect free functions (if only to help clarify where to expect side-effects)

  2. Design less with traits, more using the app’s primitive/core types (distinct from implementing traits from the std lib)

  3. Resolve what some consider an anti-pattern of returning &mut Self (is it just pass mut Self instead, or prefer a side-effect..?)

  4. How to unify a design process for state management whilst proactively designing with lifetimes (as opposed to despite or around lifetime challenges)

I agree. One point that might be helpful when describing the domain and range of the function, is to distinguish when a function is only a “partial” function. It seems to get intermingled with the question of “pure”.

The interaction of permitted inputs with the function’s implementation dictates that function’s range. The spec is often fully captured by the type signature, plus/minus help from the compiler in getting that spec right (broadly the big so what with static type checking at compile time).

With that in mind, it may be useful to treat all functions that can either panic or not return in a timely manner, as partial. Partial functions are not good in any app; obvious. But this in contrast to libraries where it’s oak to let the end user consider how to interpret the situation before proceeding. How much via Result vs a panic is of course a design/domain-driven choice.

So for instance, if the range/return type is Result<T,E> for a function that retrieves the head of a list, it can be considered “complete” if it’s capable of returning an error when presented with an empty list (or a default value if the semantics of an empty list allow for it; per a version of the above-mentioned design choices).

It’s useful to call out functions that are “partial” because it signals the need for downstream consideration. The std lib does so by declaring the possibility of a panic. Partial functions flag the need to either pre-process the input to avoid the panic-inducing failure, or anticipate some sort of try/catch (e.g., timeout using a runtime env). All of this effectively wraps the partial function in such a way as to make it complete by the time it’s executing in your app.

In the spirit of “fail fast” to create a robust, correct design, if tagging a function as “partial” enables rational use of a panic, that is perhaps a good thing... more choice anyway because “we know what to do”... don’t panic, it’s just a partial function :)). Admittedly, my point here might move as I learn more about providing the user a context in which the event occurred.

I’m not posting because we don’t already know this, but rather because terms such as “pure” and “partial” are useful because they conjure patterns for how to deal with the issues they raise. Once a reasonable set of “within these limits”* are agreed upon, we can safely reason about what to expect and how to proceed.

* everyone has the same notion of “reasonable” right? :)) - ironically, I argue that the wide range of interpretations increases the value of using these terms. A worse case is to “agree to disagree” but for nicely articulated reasons.