Hello Swift Evolution,
Right now, with integer generics, we can represent fixed-size collections, but we don’t have any way of restricting the possible values to only a specific set. I would like to propose that we extend value generics to support Bool. Recently, I’ve run into several cases where these could dramatically reduce code duplication:
Static State
I was working on a wrapper for the Vulkan API and I needed to model a VkCommandBuffer. Command buffers have multiple states and only certain calls are valid in certain states. I wanted to explicitly block the use of functions that only work in the Recording state from being used in the Executable state, and the only way to do this currently in Swift is to model these as two different types:
// This is simplified a lot, and there is way more nuance to command buffers
struct RecordingCommandBuffer {
let handle: VkCommandBuffer
init() { … }
deinit { deinit code }
consuming func finishRecording() -> ExecutableCommandBuffer {
let cb = ExecutableCommandBuffer(handle: handle)
discard self
return cb
}
}
struct ExecutableCommandBuffer {
let handle: VkCommandBuffer
fileprivate init(handle: VkCommandBuffer) { … }
deinit { the same deinit code again }
}
// There are many more commands just like this
func cmdDraw(_ cb: inout RecordingCommandBuffer) { … }
// This would actually be a function on the Queue type
func submit(_ cb: consuming ExecutableCommandBuffer) { … }
With boolean generics, this could be modeled as a generic parameter, reducing code duplication and boilerplate:
struct CommandBuffer<let isExecutable: Bool> {
let handle: VkCommandBuffer
init() where isExecutable == false { … }
deinit { deinit code }
consuming func finishRecording() -> CommandBuffer<true> where isExecutable == false {
let cb = CommandBuffer<true>(handle: handle) discard self
return cb
}
}
// There are many more commands just like this
func cmdDraw(_ cb: inout CommandBuffer<false>) { … }
(This specific example would be improved by also having labels for generic parameters, but that is a pitch for another day.)
Mutability
Another big use case of this for me is for modeling mutability. In the Swift stdlib, there are multiple types now that have a mutable and an immutable variant with perfectly duplicated code and a few specific additions (Ref/MutableRef and Span/MutableSpan). I have also needed to make similar structures for my own libraries. For example, this simplified 1-D vector type that I made with a corresponding view type:
struct Vector: ~Copyable {
...
func dot(_ other: borrowing Vector) -> Double { … }
static func +=(lhs: inout Vector, rhs: borrowing Vector) { … }
@lifetime(self)
subscript(_ range: some RangeExpression) -> VectorView<false> { … }
@lifetime(&self)
subscript(mutating range: some RangeExpression) -> VectorView<true> { … }
}
struct VectorView<let mutable: Bool>: ~Escapable, ~Copyable {
…
func dot(_ other: borrowing VectorView) -> Double { … }
static func +=(lhs: inout VectorView<true>, rhs: borrowing VectorView) { … }
}
extension VectorView: Copyable where mutable == false {}
Using this mutable parameter, we can remove the need for a mutable and immutable variant and the need to duplicate all of the non-mutating functions across both view types.
Future Directions:
Parameterize Ref and Span Mutability
In my last example, I omitted the implementation of VectorView, but it would be great if I could just use Span<Double> as the backing reference. The problem is that there is no way to pick Span or MutableSpan based on the value of the mutable parameter. To solve this, we could have Span be parameterized over its mutability and make MutableSpan just a type alias of Span with mutable set to true:
struct Span<Element, let mutable: Bool>: ~Escapable, ~Copyable { … }
extension Span: Copyable where mutable == false {}
typealias MutableSpan<Element> = Span<Element, mutable: true>
struct VectorView<let mutable: Bool>: ~Escapable, ~Copyable {
var span: Span<Double, mutable: mutable>
…
}
This would also be useful on Ref and MutableRef.
Conditionally Async Functions
I mentioned another future use-case in the thread on @Reasync. Instead of adding a new reasync modifier that works like rethrows, async can be parameterized over a boolean generic parameter. This would properly represent the way that the compiler must produce a specialization for both the async and non-async variations and could scale to more complicated cases with multiple closure arguments. For example:
func withSomeValue<T: ~Copyable, let isAsync: Bool, E: Error>(
_ body: (SomeValue) async(isAsync) throws(E) -> T
) async(isAsync) throws(E) -> T {
…
}
Enum Case Generic Values
In many cases, booleans will not have enough granularity to describe the API, so we could, in the future, allow user-defined enums as well without needing to add any complicated compile-time execution mechanism.
Boolean Expressions
Following the pitch for expressions of literals to be allowed in integer generics ([Pitch] Literal Expressions), the same concept could be implemented for boolean generics. This would allow || and && to be used in boolean generic parameter expressions. We could also allow integer operators that produce booleans like >, <, and ==.