-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
proposal: Go 2: capability annotations #24956
Comments
How does it work if I assign a variable with a set of views to an empty interface value? Do I have to have exactly the correct set of views in order to type assert the empty interface value back to the original type? Do the views have to be listed in the same order? |
Good questions. I would say that you can type-assert to a view with any subset of the concrete capabilities, with the usual caveat that element types for maps, slices and pointers must be exact. As with interfaces, the first match in a switch wins. The capabilities in a view are an unordered set, so you could enumerate them in any order. |
Another interesting question: what would |
One of the things I would like to see from any system like this is support for some form of thread safety analysis (https://clang.llvm.org/docs/ThreadSafetyAnalysis.html). Is there a way to use this syntax to say "this field requires this mutex to be held?" Unfortunately it seems kind of out of scope. |
This proposal might be possible to extend to locking invariants, but it would make the specification much more complex. As I see it, one of the advantages of this proposal is that it enforces the capabilities within the Go type system (rather than in an external tool), so extending it to thread-safety analysis would mean that we encode that analysis in the Go spec. One way to do that might be to add an package sync
type [T] Mutex struct {
WithLock(func(*T#{Getter,Setter}))
} whereas package atomic
func [Ptr] SwapPointer(addr *(Ptr#Escape)#Atomic, new Ptr#Escape) (old Ptr#Escape)
func SwapPointer[*T] (addr **T#Atomic, new *T) (old *T)
func SwapPointer[*T#Getter] (addr *(*T#{Getter,Escape})#Atomic, new *T#{Getter,Escape}) (old *T#{Getter,Escape}) |
(I've realized in writing more examples that capabilities should associate loosely rather than tightly: too many parentheses. Updating examples.) |
/cc @jaekwon |
[Edit: can too. See below.] |
/cc @wora |
On further reflection, I think this proposal can express “must be non-nil” constraints: we just have to express them as a positive capability (“can be nil”) rather than a negative constraint (“must be non-nil”) That suggests another built-in capability, as follows: Types with a When a variable of a nillable type without the
The operator sequence |
I believe this proposal also subsumes #23764. If a struct type has a field without the |
I do think the syntax for |
But this is too restrictive; it says the return type has only I think you'll need generic capability parameters:
How can I express that neither the argument nor the return value have Maybe
|
Yep, leftover from an earlier draft. Fixed. |
That's not obvious to me one way or the other, especially given the need for capability parametricity. (That is: it may be that the need for parametricity implies that pretty much all |
Are there any other languages with a facility similar to this? Typically in programming language the term "capability" refers to the right to perform some operation. These attributes seem almost like the reverse of that: rather than granting certain rights, they remove them. "Capability" may not be the best name here. The capability syntax is very general but it seems that specific capabilities have specific meanings implemented by the compiler. Are there meanings for capabilities other than those implemented by the compiler? This seems like an experimental language feature with potentially far reaching consequences, not an established, proven mechanism. As a general rule, Go has avoided adding features that are not well understood. |
Some of it is like Hermes typestates. Unfortunately, the Hermes book doesn't seem to be available online. From my copy, you could define your own typestates that the compiler would semi-automatically propagate for you, and there were some built-in ones with special meanings.
It actually resonates with me: a capability is a reference to a value along with permissions on how you can use it. So actually what Bryan calls a View is a capability (or the type of a capability): |
Can you provide an example of subsuming #23161? Trying to understand.
A Golang "Interface" is something that can be converted at runtime to a concrete type. What if we just declare a new type called a "Restriction", which is much like an Interface, except it cannot be converted back to an interface or any other type (besides a more restrictive type)? type FooInterface interface {
A()
B()
}
type BarRestriction restriction {
B()
}
type fooStruct struct {}
func (_ fooStruct) A() {}
func (_ fooStruct) B() {}
func (_ fooStruct) C() {}
func main() {
f := fooStruct{}
var fi FooInterface = f
fi.(fooStruct).C() // FooInterface doesn't include C but we can convert.
var br BarRestriction = f // OK
var br BarRestriction = fi // OK
var br BarRestriction = &fi // Pointers to interfaces don't implement restrictions.
var fi2 FooInterface = br // NOT OK
br.A() // NOT OK
br.B() // OK
br.C() // NOT OK
br.(FooInterface).A() // NOT OK
br.(fooStruct).A() // NOT OK
br.(fooStruct).A() // NOT OK
} This just saves us from declaring another structure to hold an unexposed reference to a reference object, and copying the method signatures that we want to expose from the referenced object. type Foo struct {
bar Bar
}
func (a Foo) A() { a.bar.A() } // THIS IS...
func (a Foo) B() { a.bar.B() } // ...TEDIOUS...
type Bar struct { ... }
func (b Bar) A() {}
func (b Bar) B() {}
func (b Bar) C() {} // HIDE ME! |
HP's Emily added capability verification to OCaml, but as a subtraction from the language rather than an addition. OCaml already has at least two other features that provide a similar sort of capability restriction: row polymorphism and object subtyping. With row polymorphism, capabilities are encoded as record fields (in Go, struct fields), and functions can accept input records with arbitrary additional fields (which the function cannot access). With object subtyping, capabilities are encoded as methods on an object type, and any object with at least those methods can be coerced to (but not from!) that type. Mezzo represents static read and write permissions as types (as in this proposal), but also adds a Microsoft's Koka has user-defined effect types. Its effects track capabilities at the level of “heaps” rather than variables. (Heap- or region-level effects are typical of “effect systems”, which are mainly oriented toward adding imperative features to functional languages.) Wyvern (described in this paper) checks capabilities statically, but at the level of modules rather than variables. Wikipedia has a list of other languages that implement object capabilities. Admittedly, that list is mostly research languages, and many of those languages use a dynamic rather than a static form of these capabilities. (For example, the E programming language, from which many of the others derive, uses pattern matching and Smalltalk-style dynamic dispatch to construct facets with restricted capabilities. You can think of E facets as the dynamic equivalent of the static “views” in this proposal.) Several other languages have build-in keywords that resemble the various built-in capabilities in this proposal, but do not allow for user-defined capabilities. (For example, consider The For those with ACM or IEEE library access, there are a few shorter Hermes papers available: |
There are two related concepts here: “capabilities” and “views”. A “capability” grants a (positive) permission for some method, field, or variable; a “view” restricts a variable (generally a function argument) to a particular subset of its capabilities. (I'm not attached to the naming, but I couldn't think of anything more appropriate. Please do suggest alternatives!) |
Yes: since methods and fields can be restricted to capabilities, they can have any meaning that can be expressed as a method set. That is both a strength and a weakness of this proposal: a strength in that it allows for user-defined behavior, but a weakness in that it partially overlaps with interfaces. (Views are not interfaces, however: methods removed by a view cannot be restored by a type assertion.) For example, see the |
@jaekwon The // Read-only reference to an unrestricted slice.
var myArray #&Getter []byte = make([]byte, 10)
// Read-only struct.
var myStruct #&Getter = MyStruct{...}
// Read-only pointer to a mutable struct.
var myStructP #&Getter = &MyStruct{...}
// Read-only interface variable.
var myStruct #&Getter MyInterface = MyStruct{...}
var myStructP #&Getter MyInterface = &MyStruct{...}
// Read-only function variable
var myFunc #&Getter = func(){...} |
(@jba, just so you know, I'm not ignoring your questions about |
On further consideration, I've decided to withdraw this proposal. It was motivated in part by Ian's question in #22876 (comment):
However, in order to make the result general I've had to sacrifice both conciseness and uniformity: each built-in capability and shorthand syntax would make the language less orthogonal, and without them the proposal is both too weak and too verbose: it addresses a broad set of problems but none cleanly. That's the opposite of the Go philosophy. I do think it's an interesting framework for thinking about other constraints we may want to add, but it's not a good fit itself. |
|
Background
This is a proposal about constraints on types and variables. It addresses the use-cases of “read-only” views and atomic types, but also generalizes over channel constraints in the process.
This proposal is intended to generalize and subsume many others, including (in no particular order) #22876, #20443, #21953, #24889, #21131, #23415, #23161, #22189, and to some extent #21577.
Proposal
Fields and methods on a struct type can be restricted to specific capabilities. To callers without those capabilities, those fields and methods are treated as if they were unexported and defined in some other package: they can be observed using the
reflect
package, but cannot be set, called, or used to satisfy interfaces.A view is a distinct type that restricts access to the capabilities of its underlying type.
Grammar
Capability restrictions follow the grammar:
A
Capability
precedes a method or field name in aFieldDecl
orReceiver
. A method or field without an associatedCapability
can be accessed without any capability.A
View
follows aTypeName
in aParameterDecl
,FieldDecl
,ConstDecl
,VarDecl
, orConversion
. ATypeName
without an associatedView
includes all of its capabilities. An emptyView
(written as#{}
) can only access methods and fields that are not associated with aCapability
.A
VarView
follows theIdentifierList
in aFieldDecl
orVarDecl
. It restricts the capabilities of references to the declared fields or variables themselves, independent of type. Those capabilities are also applied to the pointer produced by an (explicit or implicit) address operation on the variable or field.A package may define aliases for the views of the types it defines:
Built-in capabilities
Channel types have the built-in capabilities
Sender
andReceiver
.<-chan T
is a shorthand for(chan T)#Receiver
, andchan<- T
is a shorthand for(chan T)#Sender
. The send and close operations are restricted to theSender
capability, and the receive operation is restricted to theRecevier
capability.Slices, maps, and pointers have the built-in capabilities
Getter
andSetter
. (QUESTION: should we defined channel-like shorthands for these capabilities?) TheSetter
capability allows assignment through an index expression (for slices, maps, and pointers to arrays) or an indirection (for pointers), including implicit indirections in selectors. TheGetter
capability allows reading through an index expression, indirection, orrange
loop.Pointers to numeric, boolean, and pointer types have the built-in capability
Atomic
. TheAtomic
capability allows assignment and reading through the functions in theatomic
package, independent of theSetter
andGetter
capabilities.The
Getter
,Setter
, andAtomic
capabilities can also apply to variable and field declarations (as aVarView
). TheGetter
capability allows the variable to be read, theSetter
capability allows it to be written, and theAtomic
capability allows it to be read and written via an atomic pointer. (A variable with only theGetter
capability cannot be reassigned after declaration. A variable with only theSetter
capability is mostly useless.)The built-in
len
andcap
functions do not require any capability on their arguments. The built-inappend
function requires theSetter
capability on the destination and theGetter
capability on the source.Assignability
A view of a type is assignable to any view of the same underlying type with a subset of the same capabilities.
A function of type
F1
is assignable to a function typeF2
if:F1
andF2
have the same underlying types, andF1
are a subset of the capabilities of the parameters ofF2
, andF1
are a superset of the capabilities of the parameters ofF2
.A method of type
M1
satisfies an interface method of typeM2
if the corresponding function type ofM1
is assignable to the corresponding function type ofM2
.This implies that all views of the same type share the same concrete representation.
Capabilities of elements of map, slice, and pointer types must match exactly. For example,
[]T
is not assignable to[]T#V
: otherwise, one could write in aT#V
and read it out as aT
. (We do not want to repeat Java's covariance mistake.) We could consider relaxing that restriction based on whether theGetter
and/orSetter
capability is present, but I see no strong reason to do so in this proposal.Examples
Reflection
The capability set of a method, field, or type can be observed and manipulated through new methods on the corresponding type in the
reflect
package:Compatibility
I believe that this proposal is compatible with the Go 1 language specification. However, it would not provide much value without corresponding changes in the standard library.
Commentary
On its own, this proposal may not be adequate. As a solution for read-only slices and maps, its value without generics (https://golang.org/issue/15292) is limited: otherwise, it is not possible to write general-purpose functions that work for both read-only and read-write slices, such as those in the
bytes
andstrings
packages.The text was updated successfully, but these errors were encountered: