Referential transparency is a functional programming (FP) concept. However, most real-world FP is not “pure” FP, and still deals with state somewhere – it's just minimized, avoided, and made more explicit.
For example, a more “object-oriented” solution might design objects that keep track of some internal state. E.g. here we might have:
class Derivative {
RingBuffer<float> state = ...;
float next(float x) {
state.push(x);
...
}
}
Derivative d = ...;
while (var x = takeMeasurement()) {
printDerivative(d.next(x));
}
A more FP approach would push the state outwards. The actual derivative calculation might be a pure function that just takes a list of values:
float derivative(Iterable<float> measurements) { ... }
However, the caller might still manage local mutable state:
RingBuffer<float> measurements = ...;
while (var x = takeMeasurement()) {
measurements.push(x);
printDerivative(derivative(measurements));
}
We could also create a FP solution that abstracts over the state management, by taking an old state and returning a new state. How to do this depends a lot on the programming language. For example, in Rust I'd define opaque types for my state and return tuples from my function:
#[derive(Default)]
pub struct State {
buffer: RingBuffer<f32>
}
// takes old "State" by value
pub fn derivative(mut state: State, x: f32) -> (State, f32) {
// local mutation, does not affect caller
state.buffer.push(x);
let d = ...;
// returns new state
(state, d)
}
// usage
let mut state = State::default();
while let Some(x) = take_measurement() {
let (new_state, d) = derivative(state, x);
state = new_state; // local mutation, this is fine
print_derivative(d);
}
If you wanted to avoid the local mutation in the caller, this could be dressed up with fancy iterators avoiding the while-loop, but it's semantically equivalent.
In languages like Java or Python this would be more difficult to do cleanly, as they use reference semantics instead of value semantics. It is not possible to be exclusive owner of an object, making in-place mutation potentially unsafe. A FP solution would likely have to avoid local mutations, and instead make copies. For example in Python:
State: TypeAlias = list[float]
def derivative(state: Optional[state], x: float) -> tuple[State, float]:
# create new state
if not state:
new_state = [x]
else:
new_state = [*state[1:], x] # copy part of old state
# calculate derivative
d = ...
return new_state, d
# usage, with local mutation of the "state" variable
state = None
while (x := take_measurement()):
state, d = derivative(state, x)
print_derivative(d)
All in all, I'd recommend not thinking too much in terms of referential transparency. It is quite useful to write mostly-pure functions, since it is easier to reason about them and to test them. But a lot of actual code deals concerns stateful interaction, and has to deal with the outside world. It is not practical to have a completely pure program, you can just deal with mutable state in different ways – encapsulating the state in objects, or making the state change explicit via old state → new state
style functions. Local mutable state is also far less problematic than dealing with shared mutable object graphs. You can mix and match techniques as you see fit to achieve a useful design.