> Another thing to consider is if we have an immutable slice for types with
> methods. Whats the rules for methods for pointers? What if one of the slice
> elements modifies itself by referencing itself via a globally scoped
> pointer. Do we create a snapshot copies of the slice, and if so how can we
> update immutable slice to reflect changes made to the original mutable
> slice?
We don't make a snapshot copy. I don't see how that could be possible.
In Brad's proposal, a value of type [].T is not immutable. It simply
does not support any modification operations. As the doc says, this
is similar to a send-only channel. The existence of a value V of chan
<- int doesn't mean that there is no way to receive values from the
channel; it only means that you can't receive a value from V.
This proposal is analogous to a const char * pointer in C/C++. The
array to which such a pointer points may change, but it may not be
changed through that pointer.
This is perhaps something of a drawback to this proposal. It provides
a read-only view of a slice, but it in no way ensures that the slice
does not change. This has some utility as documentation: a function
or method that takes a parameter of type [].T is certain to not modify
the elements of that slice (barring the use of unsafe). And of course
we get the ability to pass a string value to a parameter of type
[].byte. But this is not the same as immutability. It is only the
inability to change the elements of a slice through a particular view
of that slice.
The proposal suffers from the strstr problem of C. The C function
strstr may be written as either "char *strstr(const char *, const char
*);" (the standard way) or as "char *strstr(char *, const char *);" or
as "const char *strchr(const char *, const char *);". The first
requires an unsafe operation in the implementation of strstr: a
conversion from const char * to char *; it also provides an
underhanded way to convert from "const char *" to "char *". The
second means that strstr may not be used to search inside a string
literal. The third means that the caller of strstr may need to
perform the unsafe operation. The equivalent problem in Go shows up
in bytes.Split. We can use [].byte as the first parameter, but then
we have to return [][].byte which may not be what the caller needs.
Or we can use []byte as the first parameter, but then we can't pass in
a string literal or a [].byte, although the function does not need to
modify the slice it is passed.
I suspect that these kinds of issues may be inevitable when trying to
provide a read-only version of a type in a language like Go, or C,
that does not support function overloading and does not support any
sort of polymorphism (e.g., func Split(T {[]byte, [].byte}, [].byte) T
which is intended to mean that the first parameter is either []byte or
[].byte and the result parameter has the same type as the first
parameter).
Note that there is another approach, which is to not make read-only
variants of types, but to instead have read-only values. The way this
works is to say that a value of type []T may happen to be immutable,
meaning that the values in the slice may not be modified. We permit
the free assignment of string to []byte, but the resulting value is
immutable. Any attempt to modify it fails, at compile time if
possible but otherwise at runtime. We implement this by, say,
negating the length field. Any attempt to access a value in the slice
uses the absolute value of the length field in the bounds check. Any
attempt to change a value in the slice uses the length field
unchanged, thus failing the bounds check. This approach loses the
documentation aspect of Write([].byte). The advantage is that the
type system does not become more complex. The disadvantage is that
more errors are detected only at runtime.
Ian