Hello Community,
I recently built a hobby framework on the assumption that the compiler can perform even deeply nested generic specializations, but this does not seem to work here.
In my framework RedCat, I have implemented reducers as a protocol taking only the state as an associatedtype. Actions are given to the reducer using generics:
public protocol ErasedReducer {
associatedtype State
func apply<Action : ActionProtocol>(_ action: Action,
to state: inout State,
environment: Dependencies)
...
}
There are a couple of more concrete reducer types that actually do have an Action associatedtype:
public protocol ReducerProtocol : ErasedReducer {
associatedtype Action : ActionProtocol
func apply(_ action: Action, to state: inout State)
}
public extension ReducerProtocol {
func apply<Action : ActionProtocol>(_ action: Action,
to state: inout State,
environment: Dependencies) {
guard let action = action as? Self.Action else {
return
}
apply(action, to: &state)
}
}
Finally, composed reducers that can consume different actions essentially just forward apply calls to two sub-reducers.
When I mark ReducerProtocol
's generic apply
function as inlinable, then thould be possible to highly optimize the function to the point that it will only be called when the guard condition actually succeeds - which is statically knowable.
However, this seems to only work up to a certain level of nesting of reducers. When I jus tried to compile this example project with optimizations, the disassembly showed that the dynamic cast is still there. I confirmed this by setting break points at the return statements - the break points were actually hit (which wouldn't be the case in properly optimized code).
I'm not 100% sure if it's the nesting level, but there are things that point to this as an explanation. Here's my reasoning:
The point from which I call the generic apply function is actually some kind of erasure:
public extension ErasedReducer {
func applyDynamic(_ action: ActionProtocol,
to state: inout State,
environment: Dependencies) {
...
}
}
but this should be ok, because this only calls the following:
extension ActionProtocol {
@inlinable
func apply<R : ErasedReducer>(using: R,
state: inout State,
env: Dependencies){
...
}
}
which in turn calls the reducer's generic apply method using self. And I actually confirmed the guard is actually properly eliminated using a composition of just two reducers. It doesn't work for deeply nested reducers though and I can't figure out why.
How would I work around this? What to do now?
Edit: Correction: originally, I did not test this with the composition of two reducers, but I tried to create a minimal failing example by writing a 50 lines swift package containing only the relevant code, but the generic specialization succeeded. I concluded that it had to do with the level of nesting, but it may have other reasons. For instance, the 50 lines package was in one file only. Also, the top level protocol did not have any associatedtypes.