type Foo = object
a: int
proc newFoo(): ref Foo =
new result
result.a = 3
proc alterFoo(f: ref Foo) =
f.a = 5
let foo = newFoo()
alterFoo(foo)
echo foo.a
I doubt this can make it into Version 1, but I claim it is a solved problem:
proc alterFoo(f: ref Foo) {.writes: [].} =
f.a = 5 # compiler prevent this then
Not quite. I was assuming that let always defined an immutable binding, and as such, it would be impossible to mutate a data structure which was referenced by let, even deeply (kind of what happens in Rust). Instead, it seems that if a let binding refers to a data structure that eventually contains references, that data structure is mutable.
It is not just a matter of tracking effects in functions.
I don't see any problem with the current approach. Nim hides the fact that foo is a pointer, and the foo.a is actually foo[].a (Go does the same). The let keyword protects only the pointer, not the object where it points to.
Let me remove the function. The following code compiles:
type Foo = object
a: int
proc newFoo(): ref Foo =
new result
result.a = 3
let fooPointer = newFoo()
fooPointer.a = 4
echo fooPointer.a
let fooPointer2 = new Foo
fooPointer2.a = 6
fooPointer2[].a = 7
echo fooPointer2.a
let foo = Foo(a: 8)
when false:
foo.a = 9 # 'foo.a' cannot be assigned. And that's good!
echo foo.a
My question is that why the following fails and how could this be corrected:
let fooPointer2 = new Foo(a: 5)
"I don't see any problem with the current approach... The let keyword protects only the pointer, not the object where it points to."
This is exactly the problem for me. It means that it is difficult to have guarantees that your data structure is immutable if it contains pointers somewhere (even deeply nested). I assumed the whole point of let vs var was to distinguish stuff that can mutate from stuff that cannot
This is exactly the problem for me. It means that it is difficult to have guarantees that your data structure is immutable if it contains pointers somewhere (even deeply nested)
Yes but let has been designed to model the shallow immutability of parameter passing. It was introduced for easy "evaluate once" semantics for templates. Deep immutability is much harder to support in the type system. Especially since Nim's type system strives for a good balance between convenience and compiletime guarantees.
andrea: This is exactly the problem for me. It means that it is difficult to have guarantees that your data structure is immutable if it contains pointers somewhere (even deeply nested).
This is actually pretty easy to guarantee. Simply do not expose the fields of an object or procedures/methods that alter its state.
The concern that this would actually address is to guarantee that a procedure doesn't mutate its arguments while still allowing other procedures to do it. This then is more about a procedure's contract than a property of the type.
The problem with making something entirely immutable is, as Araq notes, you may still need mutable internal state that is not exposed to the outside world (for caching, debugging, collecting performance statistics, self-organizing lists, splay trees, etc.). I.e. what you generally want is immutable abstract state while still permitting mutable concrete state. This is not an easy problem to solve for a type system. A compromise approach is what Scala does: it allows you to define individual fields of an object as something that you cannot assign to (with val), but which are still mutable if they are reference types. This allows you to build up immutable types recursively from other immutable types, but also doesn't prevent you from alternatively ensuring immutability at the semantic level (by not exposing methods that alter the state) or selectively mixing in mutable internal state.
A quick summary (please correct me, if I'm wrong): I started by misunderstanding the question (I thought that having an extra function call can expose something). Everybody agrees on that we have a problem (if a proc's parameter is not var, it can still modify data if there is a "ref" in the chain, e.g., foo[].a=10) and the language should give some help or provide a way to write better/stricter/more verbose code. Araq said that tracking effects in functions can help (I can't see how it would work in this case exactly, but we will see, and I'm generally excited about having a warning if a var is not needed), and the way I interpret his "de-constify" comment is that we should not tie our hands with too strict rules (I have Python's private field in my mind: they starts with double underscores like __privateVar, but it's only a convention, not a strict rule). Jehan had a comment about immutable class: I worked a lot with Date and ImmutableDate objects (guess which of these didn't have setters), and it is a working solution, but requires extra work for each class. I will try to check Scala to understand his other reference.
Let me propose a solution which is the deep immutability with the type system. I'm really not good at giving names, so I'll call it stone (because the data is written in stone), but this is just to describe the effect.
There are two rules (checked at compile time):
Some notes before the examples:
Examples:
type MyObj = object
someInt: int
refObj : ref MyObj
...
var myVar = MyObj(someInt: 11, refObj: ...)
var myVar2 = MyObj(someInt: 12, refObj: ...)
let myStone = stone myVar # exactly same as: let x = y, but the type is different
echo type(myStone) # prints: stone MyObj
myStone = myVar2 # INVALID: because of "let"
myStone.someInt = 99 # INVALID: because of "let"
myStone.refObj.someInt = 99 # INVALID: because type(myStone.refObj) is "stone ref MyObj"
myVar.someInt = 99 # VALID: we can change this value though myVar
myVar.refObj.someInt = 99 # VALID: we can change this value though myVar
var myVarStone = stone myVar # this is a copy of myVar, the type has changed
echo type(myVarStone) # prints: var stone MyObj
myVarStone.someInt = 55 # VALID: we have a "var". Note that myVar.someInt is not changed.
myVarStone.refObj = new MyObj # VALID: we have a "var". Note that myVar.refObj is not changed.
myVarStone = stone myVar2 # VALID: copies myVar2
myVarStone = myVar2 # VALID: equivalent to the previous line, compiler is smart enough
# to convert "var MyObj" to "var stone MyObj"
myVarStone.refObj.someInt = 99 # INVALID
var field = myVarStone.refObj
echo type(field) # prints: var stone ref MyObj
field.someInt = 99 # INVALID, because you have "stone ref" in the type
field = field.refObj # VALID, your pointer will have a different address
var x = field.someInt # VALID: you can copy anything
echo type(x) # prints "int" because "int" doesn't have a pointer embedded,
# so "stone" property is forgotten
var y = field.refObj # VALID: you can copy anything
echo type(y) # still prints: var stone ref MyObj
Any comments, ideas?
Peter
@andrea Thank you for the examples. Everything works as you describe. My point is that for initValues(ourRef: var ref ...) you can't be sure that it won't change where ourRef points. Let's assume that we would like to have ourRef.something = 5 without modifying the address of our ref (the motivation: x and y are ref, var x = y; initValues(x); assert y.something == 5). Currently the proc can't promise that. I just wanted to give more info because you took the time to answer. Now let's focus on Araq's suggestion :)
@Araq Could you provide more examples what will be allowed and what not? I know that this is a lot of work to answer the 2 questions below, but I think that this would shorten this discussion and everybody would be on the same page.
Thank you, Peter
1. Which of these function definitions will be valid?
type myType = object
i: int
r: ref myType
proc f1(x: myType) =
...
proc f2(x: var myType) =
...
proc f3(x: myType) {.writes [].} =
...
proc f4(x: var myType) {.writes [].) =
...
proc f5(x: myType) {.writes [x].} =
...
proc f6(x: var myType) {.writes [x].} =
...
proc g1(x: ref myType) =
...
proc g2(x: var ref myType) =
...
proc g3(x: ref myType) {.writes [].} =
...
proc g4(x: var ref myType) {.writes [].} =
...
proc g5(x: ref myType) {.writes [x].} =
...
proc g6(x: var ref myType) {.writes [x].} =
...
2. Which function (e.g., f1, f2, g1, etc.) will be able to contain the following lines? (And which of these will give warning?)
x.i = 5
x.r.i = 5
f1(x)
f2(x)
f3(x)
f4(x)
f5(x)
f6(x)
g1(x)
g2(x)
g3(x)
g4(x)
g5(x)
g6(x)
An example answer for the second question: f2 can contain f1(x) because this is valid:
proc f2(x: var myType) =
f1(x)
Araq: The question with this proposal is then whether it will hurt us badly that let is not compatible to the parameter passing semantics anymore.
Your idea sounds the best, IMO. But a couple of thoughts:
1) Use a {.shallow.} pragma to get parameter/shallow-semantic let statements. That wouldn't exactly be "backwards compatible" but at least existing code could be easily modified to work the same way it was originally written. eg:
let foo {.shallow.} = constructTree()
let bar = constructTree()
foo.x = 8 # allowed
bar.x = 8 # disallowed
2) Allow the let keyword in parameters to get the same "deep immutability" semantics as the let statements. This also would give you more convenient syntax than writing {.writes:[].} and, more importantly, would make it easier to indicate which parameters aren't written too, instead of which ones are (which I think people might want to do more often). eg:
proc blah(n: ref Node, m: let ref Node) =
n.x = 8 # allowed
m.x = 8 # disallowed
Just a thought...
Would it be possible to create a new generic Immutable[T] type and a set of templates/macros to help propagate immutability to subfields as well as to disallow modification?
type Immutable[T] = distinct T
proc imm*[T] (t:T):Immutable[T] {.inline.} =
result = cast[Immutable[T]](t)
template `.`*[T] (t: Immutable[T], sub:string):expr {.immediate.} =
imm(cast[T](t).sub)
# todo: check that "t.sub" isn't already Immutable
# todo: template `.=` and `=` when made possible, and [] and so on.
# Perhaps copy primitive types
If so, then, a proc could return Immutable[T] values.
On devel a new experimental mode has arrived called "strictFuncs". See also https://github.com/nim-lang/RFCs/issues/234
For example:
{.experimental: "strictFuncs".}
type
Node = ref object
le, ri: Node
data: string
func len(n: Node): int =
# valid: len does not have side effects
var it = n
while it != nil:
inc result
it = it.ri
func mut(n: Node) =
let m = n # is the statement that connected the mutation to the parameter
m.data = "yeah" # the mutation is here
# Error: 'mut' can have side effects
# an object reachable from 'n' is potentially mutated