Hi,
I hit an interesting problem recently that I think bumps against a couple of issues with Swift 4. I’m curious to hear what others think and if anything has been proposed to tackle this in future.
The tl;dr: associated types can be specified to be optional types, and this seems to break the ability to use generic constraints that use the associated types. This affects Swift-y API design.
Here’s a concrete example:
// ---- Primitives needed to demonstrate the issue
protocol Action {
associatedtype StateType
var name: String { get }
}
protocol QueryParameterCodable {
func encodeAsQueryItems() -> [URLQueryItem]
}
/// Here we constrain on the associated type
func encodeState<T>(for action: T, with state: T.StateType) where T: Action, T.StateType: QueryParameterCodable {
print("Encoded state for \(action.name): \(state.encodeAsQueryItems())")
}
// ---- Use-cases
class FancyAction: Action {
/// This works. Change it to `FanceState?` and we’re in trouble...
typealias StateType = FancyState
let name: String = "Fancy"
struct FancyState: QueryParameterCodable {
var title: String
func encodeAsQueryItems() -> [URLQueryItem] {
return [URLQueryItem(name: "title", value: title)]
}
}
}
let action = FancyAction()
let state = FancyAction.StateType(title: "Haken - Affinity")
encodeState(for: action, with: state)
So the above works, but as shown in the comments if you change StateType
to be aliased to the optional FancyType?
, you hit problems which are entirely reasonable given current Swift:
- The
encodeState
method is not available for the optional StateType. This is expected as Optional does not directly conform to QueryParameterCodable. However because you cannot constrain on optional types because they are enums, you cannot overload the function to have a version that is also generic over Optional - so you cannot workaround it this way - You cannot seemingly change the requirements for the
associatedtype StateType
to express the fact that it can be any type except Optional<_>. It does not seem possible to specify that it should be any non-Optional or non-Enum type either. - This means you apparently cannot use such an associatedtype safely for any generic constraints. If you are creating a “public” API, the developer cannot be prevented from specifying an optional that will break your generically constrained function declarations, and unfortunately this results in rather oblique compiler errors for the developer that do not make it clear that the problem is related to the Optionality they have specified.
Ultimately it is the enum-ness (enumbness?!) that is the actual problem for constraining on the associated type, but even if there were some kind of AnyNonEnum you could constrain the associatedtype with, you’ve now lost the ability for the API user to support optional values, and likely have to make all the function calls accept T.StateType?
, which unnecessarily weakens your typing and means any caller could pass nil and you’d have to precondition or similar on this for cases where it really made no sense (for certain kinds of StateType).
Essentially, and I haven’t read this anywhere so I’m probably misunderstanding, but it seems that generally speaking associatedtype
cannot be used for Optional types unless you guarantee you never use that type for generic constraints. This seems like quite an opaque but important problem.
It looks like Swift 4.1 conditional conformances can supply a workaround:
extension Optional: QueryParameterCodable where Wrapped: QueryParameterCodable {
func encodeAsQueryItems() -> [URLQueryItem] {
switch self {
case .some(let value): return value.encodeAsQueryItems()
case .none: return []
}
}
}
…but it looks like this would become pretty onerous having to add myriad extensions on Optional for every type that has an associatedtype that might be optional.
Is there any proposal or discussion elsewhere e.g. on swift-evolution, about this problem?
Cheers