-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] View/slice #12
Comments
I think that the type system already handles that just fine: one can define different procedures for |
I think that |
That won't work, |
It should work regardless of whether View is a value or ref type. If you declare an argument of type |
Of course you can, that's not the point of
|
I am not sure what your snippets should show. In your example, let v = View(a: nil, b: 0)
bar(v) you get a compilation error, as it should (even if you make About It is debatable whether this is a good choice, but it seems pretty consistent. As you have noticed, you can bypass mutability constraints by taking a mutable reference to an immutable value |
@andreaferretti It's theoretically possible to use
Yes, but you can't have immutable references in Nim. For example:
|
I was sorry to discover that import tables
let f = newTable[int,int]()
f[5] = 3 compiles. To me, this should be a compilation error (and it could be if This whole |
@andreaferretti You are creating a import tables
let f = initTable[int,int]()
f[5] = 3 |
My point is that being mutable or not should not be a property of I have seen two approaches wrt to mutability - each has its own pros and cons but I cannot understand where Nim fits in.
Nim seems to differ from this model, because when defining object types there is no place to mark field as mutable/immutable. instead it depends on the instantiation site: type Foo = object
a, b: int
var f = Foo(a: 1, b: 2)
f.b = 5 # ok
let f = Foo(a: 1, b: 2)
f.b = 5 # not ok
At first, Nim seems to have this approach. It even has overloading based on But it seems that this fails for reference types. This, in my opinion, makes the whole semantics confusing. I cannot see a reasonable explanation of the exact behaviour, nor a rationale for it. I would be happy with the Rust model, and I also would be happy with the Scala model (although it would be too breaking to introduce at this point) but I cannot make head or tails of the current Nim model. Can you provide some explanation of why things are this way? |
It doesn't need a rationale because it's the natural model when you don't have immutability in the type system. I agree it has undesirable consequences but I don't understand what you don't understand about it. Of course var vs let is about mutability but it's only shallow immutability, not deep immutability, so ptr/ref accessed in this way are mutable. Otherwise you could get the very same behaviour with a temp variable: proc doesntMutate(n: ref Node) =
# assume the compiler prevents it:
n.field = 5
# but then this is allowed?
var x = n
x.field = 5 Furthermore if |
I am not sure I understand the argument why this is a natural model - I find it pretty contrived. Moreover it looks like Nim tracks immutability in the type system (I have certainly advertised it so!)
The thing is - it is not about shallow vs deep immutability. One can have nested object types and everything is recursively immutable until you hit a pointer. It is not clear to me why going through a pointer should affect mutability. Moreover, when using libraries, one may not even know whether at some point there is a pointer indirection. It seems some reflection of the fact that Nim compiles to C - probably things are easier this way, but I cannot figure out why exactly. But I think that Nim should have its own semantics, at least because it also compiles to javascript. Look at how NIm goes out of its way to make strings and seqs value types.
Yes, this makes it easy to bypass the mechanism, but at least it is explicit. I see it like a cheap way to freeze/unfreeze objects. Of course Rust would prohibit such an operation with its borrow checker, but I don't think this is necessary.
For me, For instance all the collections I have written use a type parameter for mutator operations, like spills or everyting in cello |
I also think that current semantics |
C has nothing to do with it.
It's not about performance either, there is a subtype relation for proc add(head: var ref Node; x: ref Node) =
x.next = head
head = x Here the fact that |
Because pointer assignments only copy the pointer and so it's easy to get a mutable view on the data anyway. Note that this completely different for an
But such an easy escape mechanism means the immutability is not guaranteed at all. Every proc can declare "deeply immutable" and yet mutate it under the hood. For example an optimizer cannot rely on it at all then. Where is the gain? But let's assume your proposal would work out and would produce a better Nim language. Is it still hard to accept that the current design is not "contrived"? (The current design is pretty much Ada's design fwiw.) |
@Araq So, if I understand correctly, the reason that the immutability restriction does not propagate trough pointers is that this would be easy to bypass anyway, and pretending that it does would only lead to a false sense of security (unless one has a compiler mechanism to enforce it, like in Rust). I still happen to think that deep immutability would save some errors (bypassing the mechanism requires intention), but I now understand better the reasons for this design. I think it would be useful to document it better in the manual or tutorials - if I find the time, I will try to contribute something. Thank you! (About the linked list example: of course sometimes double indirection is needed - this is clear and I was not discussing this) |
For, say, a string, what's wrong with In any case, most cases of superfluous string copying can be solved by using The only alternative I can see is creating an immutable string type. I'm all for such an enhancement, but @Araq is against such extensions. |
It's ok, except you can't take pointer to C allocated data. And "ref
string" requires double indirection to access data, so I think my approach
is strictly better.
|
What happens if the view 'targets' a sequence, and the sequence grows? Even with the additional |
Well all I have to say about this I already did in this issue: nim-lang/Nim#5437 TL;DR
Well I think that could be connected to an overuse of |
IMO, View and openarray concepts are related. When you accept it as as argument it is pretty much the same thing. If one could return an openarray from proc it would close the gap between the two. proc subseq[T](seq[T], i: int): openarray[T] # returns a subset of seq without copying Maybe, it is worth to extend existing openarray concept to match View features? |
I think the default slicing operator should use a type like this: let myView: View[char] = myString[3..17]
let myViewAsString: string = myView # maybe this is a good place for a `converter` function Doing this would cause some backwards incompatibility though :/ |
Chiming in, It would be very nice if fixed size slices did not allocate a seq on the heap. Example: type Uint256 = distinct array[32, byte]
# For demo purpose, normally it should take endianness into account
proc readUint256BE*(ba: array[32, byte]): UInt256 =
## For demo purpose, normally it should take endianness into account
Uint256(ba)
proc decode_public_key(pubKey: array[64, byte]): array[2, UInt256] =
result[0] = readUint256BE pubKey[0 ..< 32]
result[1] = readUint256BE pubKey[32 ..< 64]
var a: array[64, byte]
echo decode_public_key(a) Error:
|
We now have --experimental:view types and also a decent solution for deep immutability, closing. |
It's very hard to avoid accidental copies when using
system.string
type. This makes it unsuitable for I/O, especially when moving around large blobs of data.There is
system.shallow(string)
, which helps a bit, but it's semantics are underspecified and I've struggled to use it. Anyway, I think it's bad idea to mark strings as copying/non-copying at runtime instead of at type level.Therefore I propose inclusion of new
View[T]
type to the standard library.View[T]
essentially points to an array in memory and in most basic formView[T] = tuple[pointer: ptr T, length: int]
. It is modelled after Go slice and after my implementation ofView[T]
in collections.nim. Similar ideas were discussed before e.g. 1, 2.The requirements for the type are as follows:
slice(self: View, start: int, length: int)
operating, which returns subview ofself
.slice
checks for index of bounds errorsIt's easy to implement type that fullfils these requirements. However, there are two question:
ConstView
andVarView
types (to mark immutability in type system)?GcView?
The basic version, implemented as
View[T] = tuple[pointer: ptr T, length: int]
, is not memory-safe when not used carefully.It's possible to add third field
gcptr: RootRef
, that can be used force GC to keep memory pointed to byView
alive. I've tested this approach before ingo2nim
branch of collections.nim: gcptrs, goslice (go2nim was my attempt at creating Go to Nim translator).The main disadvantage is that this increases size of the type to 3 words (e.g. 24 bytes on amd64) and makes making copies a bit more expensive. Maybe we should create separate types
ViewPtr
andViewRef
?The text was updated successfully, but these errors were encountered: