Could someone point me to a discussion of why var parameters exist in Nim? I understand how they're used. I just don't understand what would be wrong with using ptr instead. Do people hate addr() that much?
My position is that I don't want to wonder about side-effects. In C++, I often advocate the Google Style Guide recommendation of pointers for output parameters, for transparency. The counter-argument -- which I hear every single time I suggest this -- is that you cannot be sure that references were not used by the library developer, so you have to read the function signatures anyway. In other words, the presence of references in the C++ language destroys the value of an explicit calling style.
Am I crazy?
# call Nim proc with parameter doSomething(par1) // call C function with pointer parameter doSomething(&par1);
foo(x)
Does foo alter x?
Exactly! It lacks transparency. I understand your point about the unsafety of ptr, but I wish that callers had to do something special in order to pass a var parameter.
They do - the variable has to be mutable, e.g., declared using "var". If you don't want your variable modified, use "let".
As for "lack of transparency", you have to know what parameter types procedures take anyway, so you'll have to look at the procedure declaration at some point.
cdunn2001: Exactly! It lacks transparency. I understand your point about the unsafety of ptr, but I wish that callers had to do something special in order to pass a var parameter.
Pointers don't help you with that, nor does C#-style annotating of the call site. Anything heap-allocated will be passed in as a reference with mutable components, anyway. And that's necessary for a lot of applications, too (such as anything that embeds a cache, even if it appears to be immutable to the outside, or self-organizing lists, splay trees, etc.). Language mechanisms cannot substitute for a well-designed API with proper contracts. An example would be command-query separation.
As for "lack of transparency", you have to know what parameter types procedures take anyway, so you'll have to look at the procedure declaration at some point.
That statement can not really convince me: When proof reading source code, we generally see what type a parameter has because its type declaration is only a few lines away, and we know that the type is valid, because compiler does not complain. But we do not know if it a var parameter without consulting the proc declaration of the library. We may argue that we always should consult that declaration, and that a fine IDE may make that easy. I am not sure if I should really love that. Maybe putting a special marker symbol before or after each var proc parameter would be not that bad? Of course smart syntax highlight editors may mark it automatically if library declaration is available. But should we rely that much on a smart IDE?
import hashes
type HashedString* =
ref object
hashCode: int
value*: string
when defined(profileHashing):
hashCalls: int
proc hash*(s: HashedString): int =
when defined(profileHashing):
s.hashCalls += 1
s.hash
proc newHashedString*(s: string): HashedString =
HashedString(hashCode: hash(s), value: s)
Stefan_Salewski: But we do not know if it a var parameter without consulting the proc declaration of the library.
There's a lot more that we don't know about the proc without looking at it. Does algorithm.sort sort in descending or ascending order by default? Do you happen to know without looking at the comments whether cmp or cmp[string] is faster for sorting strings? Does math.sqrt(-1) return NaN or raise an exception? Does the first parameter to readLine(f: File, s: var String): bool require the string parameter to be initialized? What is the meaning of 3 in substr(s, 3), the first three characters, the substring starting at the third character, or just s[3] as a string?
Mind you, I do see the benefits of being able to tell more about the behavior of a procedure from the way its being invoked, but this is very much a non-trivial problem.
I can remember some C user say: For C functions with pointer parameters the address operator & shows us that the parameter may be modified.
Then this C user has no idea of how C works:
void baz(int* a);
void foo(int* a) {
// does it modify '*a'? who knows. Note that it's already a pointer so no & are passed around.
baz(a);
}
#define bar(x) x = 8
int main() {
int a[1];
// arrays are "pass by pointer"
foo(a);
// macros can hide it too:
int x = 89;
bar(x);
}
Here is my point:
> foo(x)
> Does foo alter x?
And why is that THE most important question? More important than "does foo make DB accesses?" "Is foo time dependent?" "Can foo allocate memory?" "Is foo executing some external process?"
There was a fellow named Benjamin Adamson at Amazon who used to ask everyone to add const everywhere possible in their C++ code, including local variables and parameters passed by value. I really appreciated his persistence. Too many coders re-use variables in the middle of a function. (Ben is now a fan of Rust, last I heard.) But your suggestion is merely that, a suggestion.
@Jehan and @Araq, true.
I found what I was looking for under Common Criticisms in the wiki. Fair enough.
The underlying problem that we are dealing with here is the difference between the abstract state and concrete state of a type. The concrete state is the actual internal representation – the bits and bytes in memory – while the abstract state is the the externally visible behavior of a type, and the concrete state can change without affecting the abstract state (e.g. because of memoization) [1]. When we conceptually talk about immutability, what we generally are interested in is immutability of the abstract state; however, this type of immutability is a lot harder to capture than immutability of the concrete state, because it's beyond the capabilities of any type system that can't do full theorem proving.
Most solutions need to therefore find one compromise or another; some languages have an escape mechanism (e.g., mutable fields in OCaml or C++) in Nim's case there is a sublanguage (not hampered by verbosity) that guarantees immutability of concrete state (in a way, ref is a mutability escape mechanism for Nim). Neither solution is perfect and cannot be; you get rid of some clerical errors, but may encourage others. Having something with the simplicity of a type system and the power of a full theorem prover is one of the perpetual motion machines of computer science: it would be very nice to have, but is unattainable in practice. Hence we get compromise solutions.
[1] Similar issues arise with the modification of global state as a side effect.