So ... since this idea is not yet RFC-ready, let me post my recent musings here: Immutability in Nim is not attached to a type, but to a "symbol kind", let variables are immutable, var variables are mutable, parameters are immutable unless var and the world is nice and clean and this system works exceptionally well.
However, there is one problem: ref. Nim's immutability is deep until the first ref comes into play. This is a loophole in Nim that we could fix. We have known an algorithm for computing "deep immutabilty" (so called "write tracking") for quite some time now, but it never was obvious about how to add it syntactically to Nim nor what the default should be.
So here is my idea: Extend the immutability to ref in parameters and introduce a single symbol called mut to make them mutable. mut would be a parameter passing mode, it's not a general type constructor, so the following will not compile:
type
Node = ref object
le, ri: mut Node
Some examples that would compile:
type
Node = ref object
data: string
le, ri: Node
proc m(n: mut Node) =
n.data = "xyz"
proc depth(n: Node): int =
(if n == nil: 0 else: max(depth(n.le), depth(n.ri) + 1)
Some examples that would not compile:
proc depth(n: Node): int =
m(n) # error: cannot pass 'n' to a 'mut Node'
result = (if n == nil: 0 else: max(depth(n.le), depth(n.ri) + 1)
proc main =
let n = Node(data: "abc", le: nil, ri: nil)
n.data = "xyz" # error: cannot mutate via a ``let`` binding.
As you can see, the concept naturally extends to let variables, these can become deeply immutable too.
var T is a hidden pointer and so var ref T creates a pointer to a pointer. This has negative effects on the subtype relation, you cannot pass a subtype of ref T to a var ref T.
Orthogonality, func is a shortcut for .noSideEffect and Nim also has iterators, methods, converters, that would also benefit from deep immutabilty.
The effect system doesn't work too well with generics and produces harder to understand error messages for newcomers, I don't want to extend it even further. Furthermore the computed effects currently do not cover parameter passing semantics, parameter passing semantics are too important IMO, they are better spelt out explicitly.
Deep immutability is great feature!
var T is seems more consistent, but mut T is fine.
The interplay of let and var get interesting with deep immutability:
While this must not compile:
proc p1(n: Node) =
let x = n
var v = y
v.data = "change" # invalid, possible alias of ``n``
This one could or could not:
proc p2() =
let x = Node(...)
var v = x # valid
v.data = "change" # invalid, declare ``x`` as ``var`` to make it compile?
And here is a real problem ;-)
proc select(a, b: Node): Node =
result = if oracle(): a else: b
proc construct(a, b: Node): Node =
result = Node(data: "new", le: a, ri: b)
proc harmless(a, b: Node) =
var x = construct(a, b)
x.data = "mutated"
proc harmful(a, b: Node) =
var x = select(a, b)
x.data = "mutated"
How do we know that harmful must not compile but that harmless should compile? In my write tracking analysis I propose yet-another effect, returnsNew that would be computed for construct but if we don't want to add more effects, what else could we do? We could reject harmless conservatively, but that seems to be problematic for real-world code out there.
The language is fine as it is.
Indeed. I'll repeat what I said in #nim: leave this for after 1.0.
Same in Nim, you can do:
proc mutate(o: NodeObj) =
o.field1 = newVal # doesn't compile
so the compiler needn't pass pointer of pointer to caller.
That's required for consistency with the rest of the language and it's a feature on its own. Don't change this.
proc addListNode(head: var Node; elem: Node)
I am looking for a smooth way to pass a value via indirection - indiscriminetaly, if the value is placed on the stack, on the heap, if it is declared via let or var, all that should not matter. What is the nim-way to do this?
By the way, "var Node" is somewhat aggressive. With "var", the callee has the right to deallocate the object, so the callee gets transferred ownership (and can lay the referred object in ruins). In most cases that goes too far.
proc p2(x: mut Node) =
x.left = nil # invalid
x = Node() # valid
proc p3(x: var mut Node) =
x.left = nil # valid
x = Node() # valid
I am looking for a smooth way to pass a value as a parameter via indirection - indiscriminetaly, if the value is placed on the stack, on the heap, if it is declared via let or var, all that should not matter. What is the nim-way to do this?
You just pass the parameter. The compiler decides for you whether a readonly indirection (const& in C++ lingo) is better or not.
You just pass the parameter. The compiler decides for you whether a readonly indirection (const& in C++ lingo) is better or not.
That's why my void mutate(const ObjType& o) and your proc mutate(o: NodeObj) are equivalent. Now I get it.
You just pass the parameter. The compiler decides for you whether a readonly indirection (const& in C++ lingo) is better or not.
This explains a lot. But then, for efficiency reasons, it should be possible to "open" the passed parameter as mutable - as it is in C . Instead of writing :
var myvar := my_passed_parameter
it should be possible to write at the very beginning function body
mut my_passed_parameter -- or list version mut mypar1, mypar2,mypar3 -- no types needed, since available from the parameter list
and then the compiler can make a decision: A value already placed on the stack, eg. a simple int32, doesn't need a copy. If a val ptr had been passed by the caller , the compiler needs to copy the value and to place it on the stack. Or the compiler takes this into account early and adapts the declaration in the param list accordingly, avoiding any copy in the function itself. Yeah, nice optimization steps are possible...