I was re-reviewing a somewhat upvoted answer of mine where I attempt to explain the differences between pure, impure, deterministic, non-deterministic, and idempotent functions.
In my answer I use .NET's or Java's List<T>.Add( T )
as an example of an impure deterministic function.
I also use Random.Next()
as an example of an impure nondeterministic function.
I now think both examples are wrong.
Part of my uncertainty comes from how the this
parameter is syntactically hidden in most languages with classes and instance-methods. But at the same time, whether or not this
is hidden doesn't affect a function's behaviour: it's trivial to show that a static
method or free function taking an explicit const me
parameter is identical to an instance method with a hidden const this
parameter.
...but when this
is mutable, I'm unsure. Actually, in the general case of a free-function that accepts a reference to a mutable structure: are those mutations considered (impure) side-effects even if they aren't "hidden"?
So, is List<T>.Add( T )
impure and deterministic? Or pure and non-deterministic?
Assuming that List<T>.Add( T item )
and its static equivalent static void Add( List<T> me, T item )
are considered identical, then it follows that destructing me
to separate parameters shouldn't make a difference either, so therefore the following 3 methods are identical, no?
void Add( T item )
static void Add( List<T>* me, T item )
static void Add( T[]** array, size_t* index, T item )
array
isT[]**
rather thanT[]*
to allow for reallocating when resizing.
I also have contradictory arguments going-on in my head:
- "It's impure" because
Add
mutates state. - "It's pure" because
Add
doesn't mutate any hidden or private state: it mutates only its inputs, therefore it's pure.
If mutating objects passed by-reference (namely T[]** array
) is what matters, then what about if static void Add
instead returns a tuple of array
and index
, e.g. static ( T[]* nextArr, size_t nextIndex ) Add( T[]* array, size_t* index, T item )
(thus shifting the responsibility of reassigning this.array
to nextArr
onto the caller), but it still mutates array
's contents if the resize/reallocation wasn't necessary, but there's no hidden mutations going on, so I'm finding it very hard to say if it's now pure or not...
Similarly for determinism:
"It's deterministic" because identical input will always result in identical output: that is, if the inner structure's
T[]
array andint index
are considered input, then the end-result output is the same (i.e.[out] index = [in] index + 1
and[out] array = [in] array OR [new] resizedArray
)."It's nondeterministic" because the caller has no way of knowing if the
.Add
call will fail due to the array resizing (allocate, copy, deallocate) operation will fail due to unpredictable underlying runtime state, e.g. memory fragmentation, even if the length and contents of the input array is identical to previousAdd
calls.
In languages like C# and Java, functions generally do not have their own state, so API design horrors like strtok
can't happen again.
...except when they do: both C# and Java allow the creation of closures, which means that what looks like a stateless function-pointer/delegate type really has a bound hidden this
(👀) parameter pointing to the closure capture object, which in-turn probably also contains the original this
too - which gives me a headache because if List<T>.Add( T )
is-or-is-not pure or impure and deterministic or non-deterministic because of its this
parameter, how is a closure any different?
Oh, and both C# and Java also have thread-local storage - which allows for oddly-behaved concurrent but not necessarily reentrant functions, but I'm happy to keep that out-of-scope for now.
FnOnce, FnMut, Fn
which (are traits that) represent the possible modifications allowed in a closure. AFnOnce
closure, takes ownership of some cariable it captures from the outside - essentially "eating away" the variable (rendering it useless after calling the closure). AFnMut
does not eat away variables, but is allowed to mutate their internal state - and finnalyFn
cannot mutate anything. Using those definitions it is easier to categorize closures (remember that closure cannot mutate themselves, they are code) $\endgroup$