Hi fellow Swifters!
I would like to pitch isolated non-actor types (struct
, enum
, and, if it makes sense, class
types) in a similar fashion to functions with an isolated
parameter.
The Problem
I am working on a graph-building library where the graph is declared as an actor
and where nodes are value types that reference the graph. Clients only interact with the nodes; the graph is mostly an implementation detail of the library. A node has some properties and operations in its public API.
actor Graph {
var nodesByLocation: [Location : Node] = [:]
func degreeOfNode(at location: Location) -> Int { … }
func processNode(at location: Location) { … }
}
public struct Node {
var graph: Graph
var location: Location
public var degree: Int {
get async { await graph.degreeOfNode(at: location) }
}
public func process() async {
await graph.processNode(at: location)
}
}
A client wants to access some properties and run some operations on the node. We assume that this combination of steps is client-specific and not something the library would offer. Since the graph is an isolated, the client has to introduce a suspension point at each step.
let node: Node = …
if await node.degree > 0 { // client-specific condition
await node.process()
}
This is undesirable: the degree
might change between the if
-branch and the process()
call, possibly violating a precondition that the client wants to satisfy!
To avoid the extra suspension point, the client would need to perform the two operations directly on the Graph
and from within an isolated function.
func processConditionally(graph: isolated Graph, location: Location) {
if graph.degreeOfNode(at: location) > 0 {
graph.processNode(at: location)
}
}
let node: Node = …
await processConditionally(graph: node.graph, location: node.location)
The library needs to expand its public API more and we lose the view-like abstraction that Node
provides. It becomes unworkable as the library evolves.
A Possible Solution
The client should be able to define a Node
-isolated function. Node
is not an actor but can get its isolation from Graph
by declaring (exactly) one stored isolated Graph
property, akin to an isolated
parameter.
Methods and accessors can opt in to the isolation with an isolated
modifier, unlike in actors where you need to opt out with nonisolated
. This provides consistency with other non-actor types with an isolated
property.
public struct Node {
var graph: isolated Graph
var location: Location
public isolated var degree: Int { // isolated self, implies isolated graph
graph.degreeOfNode(at: location)
}
public isolated func process() { // isolated self, implies isolated graph
graph.processNode(at: location)
}
func myNonisolatedMethod() async { // nonisolated
await graph.processNode(at: location)
}
}
The client can now define a Node
-isolated function.
func checkAndProcess(_ node: isolated Node) {
if node.degree > 0 {
node.process()
}
}
let node: Node = …
await checkAndProcess(node)
Thoughts?