Hello,
In this post I share some hesitations I'm facing when designing/adapting apis for strict concurrency checks and in particular SE-0412.
I'll first expose the problem, based on a real example so that this post is not too abstract.
I'll then explore the various solutions I could identify, and describe how each one of them has undesired consequences.
Finally, I'll ask language designers what is their opinion on the topic.
The problem
I have two public protocols and one support type that is Sendable:
/// A type that can enter the SELECT clause of an SQL query.
public protocol SQLSelectable { }
/// A type that is tied to a database table and
/// builds database queries.
public protocol TableRecord {
static var databaseTableName: String { get }
static var databaseSelection: [any SQLSelectable] { get }
}
public struct Column: Sendable { }
You can see the static properties of TableRecord
in effect below:
struct Player: TableRecord, ... { }
// SELECT * FROM player
// ^ ^ Player.databaseTableName
// Player.databaseSelection
let players: [Player] = try Player.fetchAll(db)
Enter a user who customizes their record type:
struct Player: TableRecord {
// Intent: SELECT id, name FROM player
// instead of SELECT * FROM player
static let databaseSelection: [any SQLSelectable] = [
Column("id"), Column("name")
]
}
Finally, enter SE-0412
, which creates a warning in user's code:
Static property 'databaseSelection' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6
The solutions
User defines the stored property as nonisolated
The user can make the static property nonisolated:
struct Player: TableRecord {
- static let databaseSelection: [any SQLSelectable] = [
+ // This is safe because `Column` is Sendable.
+ nonisolated(unsafe) static let databaseSelection: [any SQLSelectable] = [
Column("id"), Column("name")
]
}
This removes the compiler warning.
The user have to change the code of their record definition (SE-0412 is not, strictly speaking, source-breaking, but it creates churn).
The user uses an unsafe language feature.
The user either writes a comment explaining why this code is safe nevertheless, either leaves some code that will raise a concern in the mind of all future readers.
The rigorous user has to spend time convincing theirselves that the code is indeed safe.
The library looks ill-designed.
User turns the stored property into a computed property
The user can replace the let
stored property with a var
computed one:
struct Player: TableRecord {
- static let databaseSelection: [any SQLSelectable] = [
- Column("id"), Column("name")
- ]
+ static var databaseSelection: [any SQLSelectable] {
+ [Column("id"), Column("name")]
+ }
}
This removes the compiler warning.
The user have to change the code of their record definition (SE-0412 is not, strictly speaking, source-breaking, but it creates churn).
The library looks ill-designed.
The
var
computed property instantiates a lots of arrays, instead of a single one with the storedlet
property.
Library makes SQLSelectable Sendable
The library modifies SQLSelectable
so that it is Sendable:
-public protocol SQLSelectable { }
+public protocol SQLSelectable: Sendable { }
This removes the compiler warning.
The user does not have to change the code of their record definition.
The library has introduced a
Sendable
requirement without good justification. Not only this requirement is not needed, but it creates churn inside the library in order to solve the concurrency warnings created by this extra conformance.The library has to remove support for
NSString
andNSData
as selectable values. Not a big deal, but a possible inconvenience for users who deal with some legacy ObjC code.Users who define mutable classes that conform to
SQLSelectable
have to make themSendable
, which is not a trivial task. This is even more vexing, considering that the library did not needSQLSelectable
to beSendable
in the first place (as mentioned above).
Library has databaseSelection require Sendable values
The library modifies databaseSelection
so that it requires Sendable values:
public protocol TableRecord {
- static var databaseSelection: [any SQLSelectable] { get }
+ static var databaseSelection: [any SQLSelectable & Sendable] { get }
}
The user modifies their code accordingly, so that the compiler sees the customization:
struct Player: TableRecord {
- static let databaseSelection: [any SQLSelectable] = [
+ static let databaseSelection: [any SQLSelectable & Sendable] = [
Column("id"), Column("name")
]
}
This removes the compiler warning.
The user have to change the code of their record definition, otherwise the default implementation (
SELECT *
) kicks in.The library has introduced an unneeded
Sendable
requirement without good justification.(An old complain) the language does not see that protocol requirements have changed, and won't guide the user towards the proper resolution.
The mention of
[any SQLSelectable & Sendable]
(a mouthful) in user's code is mandatory, so that the compiler does not miss the customization point.The library looks verbose.
The library has to mention this change in the upgrading guide.
Some users will not read the upgrading guide and will wonder how to solve the initial warning.
Isolating databaseSelection to a global actor
This is a no go. The library users enjoy parallel database accesses, as well as synchronous database accesses, two features that are too precious to give up. AFAIK, those features are unsupported by global actors.
Conclusion
Did I miss a solution that has no (or less) drawbacks?
Of course, I wish the compiler would understand that this code is safe:
public protocol SQLSelectable { }
public protocol TableRecord {
static var databaseTableName: String { get }
static var databaseSelection: [any SQLSelectable] { get }
}
public struct Column: Sendable { }
struct Player: TableRecord {
// Safe, because the actual values ARE sendable.
static let databaseSelection: [any SQLSelectable] = [
Column("id"), Column("name")
]
}
What is your opinion? And in particular, do you think the compiler could learn that the above construct is safe?