What am I doing wrong with this generic constraint?

Hi,

I'm hitting a problem I don't understand with generic constraints, and wondered what I'm doing wrong.

public typealias IntentActionExecutor = (_ input: INIntent, _ presenter: IntentResultPresenter) -> Void

class IntentMappings {
    var mappings: [ObjectIdentifier:IntentActionExecutor] = [:]
    
    func forward<FeatureType, ActionType>(_ intentType: INIntent.Type, to binding: StaticActionBinding<FeatureType, ActionType>)
           where ActionType.InputType: INIntent & FlintLoggable,
                      ActionType.PresenterType: IntentResultPresenter {
        let executor: IntentActionExecutor = { input, presenter in
            // This line shows "Cannot convert value of type 'INIntent' to specified type 'ActionType.InputType'"
            let _input: ActionType.InputType = input
            binding.perform(input: _input, presenter: presenter, completion: nil)
        }
        mappings[ObjectIdentifier(intentType)] = executor
    }
}

/// supporting type info:
public protocol Action {
    associatedtype InputType: FlintLoggable = NoInput
    ..
}

public protocol FlintLoggable {
    var loggingDescription: String { get }
    var loggingInfo: [String:String]? { get }
}

public struct StaticActionBinding<FeatureType, ActionType>: CustomDebugStringConvertible 
     where FeatureType: FeatureDefinition, ActionType: Action {
...
}

I don't understand why it thinks the input is not compatible with ActionType.InputType because the function is constrained on InputType to types that are compatible.

First thing I notice is that ActionType is not actually constrained to conform to Action.

Second thing I notice is that Action.InputType must always conform to FlintLoggable, so the constraint that ActionType.InputType must conform to FlintLoggable will become redundant once ActionType is made to conform to Action.

@Nevin It is, through StaticActionBinding.


You don't seem to mention what INIntent is, but what I suspect here is probably a situation similar to this:

protocol P {
  associatedtype R
}

func foo<T: P>(_ arg: T) where T.R: Collection {
  let bar: T.R = [4, 5, 6]
}

Here, you can't assign to a variable of type T.R because you don't know what type it will actually be. T.R isn't an opened existential type, it's some concrete type that satisfies the given constraints and is only known at runtime. Which in turn means the conformance of [4, 5, 6] to Collection isn't sufficient, even if I constrain all associated types to match the type I am assigning.

You don't constrain IntentActionExecutor's input to be the same type as ActionType.InputType. But this line requires it (let me write out the types):

let executor: IntentActionExecutor = { (input: INIntent, presenter: IntentResultPresenter) -> Void in
  let _input: ActionType.InputType = input // input has type `INIntent`
  // ...
}

Use a generic typealias instead:

public typealias IntentActionExecutor<Input, Presenter> = (_ input: Input, _ presenter: Presenter) -> Void where Input: INIntent, Presenter: IntentResultPresenter

Then create your closure using:

let executor: IntentActionExecutor<ActionType.InputType, ActionType.PresenterType> = { input, presenter in
  let _input: ActionType.InputType = input // input now has type `ActionType.InputType`
  // ...
}

Or re-evaluate whether or not you really need these to be associated types and bound with generics.

Thanks for the responses. I know it's a hard sample to grok. Some detail;

  1. Yes as pointed out, the type of ActionType.InputType is inferred from the StaticActionBinding<F,A> argument in fact the compiler won't let you specify the constraint for this because it is implied.

  2. INIntent is from SiriKit on Apple platforms

  3. I do not believe the problem is due to lack of type information (unless it is a bug). See below.

I noticed the IntentActionExecutor typealias was not including FlintLoggable as a requirement on the input arg, so I rewrote it thus:

public typealias LoggableIntent = INIntent & FlintLoggable

public typealias IntentActionExecutor = (_ input: LoggableIntent, _ presenter: IntentResultPresenter) -> Void

class IntentMappings {
    var mappings: [ObjectIdentifier:IntentActionExecutor] = [:]
    
    func forward<FeatureType, ActionType>(_ intentType: INIntent.Type, to binding: StaticActionBinding<FeatureType, ActionType>) where ActionType.InputType: LoggableIntent, ActionType.PresenterType: IntentResultPresenter {
        let executor: IntentActionExecutor = { input, presenter in
            let _input: ActionType.InputType = input
            binding.perform(input: _input, presenter: presenter, completion: nil)
        }
        mappings[ObjectIdentifier(intentType)] = executor
    }
}

What we see here is that ActionType.IntentType is specifically constrained to LoggableIntent. The IntentActionExecutor typealias also expects exactly this type. And yet the assignment still fails:

Cannot convert value of type 'LoggableIntent' (aka 'INIntent & FlintLoggable') to specified type 'ActionType.InputType'

This is despite the function containing the code being constrained on exactly this type.

I fully expect I am hitting a limitation of Swift here and/or my understanding of the generics, but I would like to understand which!

It's as though the compiler cannot see that ActionType.InputType is exactly LoggableIntent, the type used in the IntentActionExecutor typealias.

Karl - you are a genius. The generic typealias solves this immediate problem and I'm trying to understand why it is required.

However of course it now means I can't put the executor into a collection but that's another story :)

The generic typealias fixes the issue because otherwise the IntentActionExecutor's first argument is not required to be the same as ActionType.InputType - it only requires a LoggableIntent. As far as the type-system is concerned, you could call the same executor several times with different kinds of intents. Look at my post again where I write out the inferred argument types.

The generic typealias constrains the closure so it only accepts arguments with the desired type. The type-system will prevent you from calling it with a some different type.

Alternatively, you could keep the closure's formal type flexible, and cast to the expected type:

if let _input = input as? ActionType.InputType {
  binding.perform(...)
} else {
  assertionFailure("Executor called with unexpected type \(type(of: input)). Expected \(ActionType.InputType.self).")
}

As far as the type-system is concerned, you could call the same executor several times with different kinds of intents.

Karl - thank you for that key sentence explaining the problem to me. I get it now.