You're converting AppSub<ParentSub.Event> to ParentSub, which would fail whenever ParentSub is not an AppSub instance (hence the need for force unwrapping). Not sure how I can fix that, though. It depends on whether you have only one generic conforming to Sub (AppSub), and whether you really need Sub protocol (which seems quite tricky).
The Sub type is intended to be framework code as opposed to AppSub which would be in application code so it's not possible to eliminate the Sub type.
The intention is really to return a Sub<ParentEvent> but I'm not sure how to achieve this with Swift. Possibilities seem to be opaque types or implementing an AnySub. As far as I can tell opaque types don't support generics? So it looks like AnySub might be the way to go? In that case I suppose Sub would look like
I believe you are running into the problem that opaque types were intended to solve (but do not, in fact, solve, except for SwiftUI).
You want the map method to return some value whose type conforms to Sub. You want that type's Event to be the ParentEvent of the lift argument's type. And that's all you really want to say about the return type. You want the implementation of map to get to pick the exact return type, because it might be different for each implementation of map.
This is exactly what opaque returns types are for: letting the method choose the return type, and only promising that the type chosen conforms to a protocol. You want to declare this:
func map<ParentEvent>(_ lift: @escaping (Event) -> ParentEvent)
-> some Sub where some Sub.Event == ParentEvent
but that's not legal Swift.
I think your solution is going to have to involve an AnySub type eraser.
If map actually needs to be implemented differently for each Sub-conforming type, then I think you will have to use AnySub. But if in fact you can implement map once for all Sub-conforming types, then you could also use Combine's solution. Change map from a protocol requirement to an extension method:
extension Sub {
func map<NewEvent>(_ transform: @escaping (Event) -> NewEvent) -> MapSub<Self, NewEvent> {
return MapSub(upstream: self, transform: transform)
}
}
struct MapSub<Upstream: Sub, Event>: Sub {
var upstream: Upstream
var transform: (Upstream.Event) -> Event
// implement Sub's requirements here
}
You'll still end up wanting AnySub, but you won't need it as often.
Right, that's confirmed my suspicions then. That's a good point about using an extension function. I've been planning to allow for multiple Events to be emitted from Subs which would require custom mapping. Perhaps that's a goal not worth persuing though if it significantly increases the complexity of the API (it would push the map implementation into app code rather than the framework).
I haven't implemented a type eraser yet but it looks like it's a rite of passage for Swift devs Hopefully one day generic protocols will happen
I've managed to get AppSub looking a bit clearer now, it separates the generics handling for the protocol from the implementation so that type inference can be used.
enum AppSub<EventType>: Sub {
typealias Event = EventType
case discoveredDevices((BluetoothDevice) -> Event)
case receivedDeviceData((BluetoothData) -> Event)
case currentTime(every: Int, action: (Date) -> Event)
func map<ParentEvent>(_ lift: @escaping (Event) -> ParentEvent) -> AnySub<ParentEvent> {
return doMap(lift).eraseToAnySub()
}
func doMap<ParentEvent>(_ lift: @escaping (Event) -> ParentEvent) -> AppSub<ParentEvent> {
switch self {
case .discoveredDevices(let cb): return .discoveredDevices({ device in lift(cb(device)) })
case .receivedDeviceData(let cb): return .receivedDeviceData({ data in lift(cb(data)) })
case .currentTime(every: let period, action: let cb): return .currentTime(every: period, action: { currentTime in lift(cb(currentTime)) })
}
}
}