Lost in the Generic Jungle

Greetings.

I am creating a framework and making heavy use of generics; something I am well used to after many years of expreience in C#, before coming to Swift when it was first launched.

I am getting an error on the declaration of the CommandSet class, which is proving difficult to resolve :

Type 'CommandSet<subjectT, commandIdentifierT, executionContextT, commandT>' does not conform to protocol 'NotifyCommandChanged'

This is accompanied by two "hints"

Unable to infer associated type 'CommandType' for protocol 'NotifyCommandChanged'

… on the declaration of the CommandType associatedtype in NotifyCommandChanged

Inferred type 'commandT' (by matching requirement 'commandChanged') is invalid: does not inherit from 'Command<Self.SubjectType, Self.IdentifierType, Self.ExecutionContextType>'

… on the commandChanged lazy var in CommandSet

I have been working on this for over a day now and would be very grateful if anyone out there can help me see what I am missing.

public protocol StringEnumType : Hashable, RawRepresentable where Self.RawValue == String { }

public protocol ExecutionContext { }

open class Command<subjectT, identifierT : StringEnumType, executionContextT : ExecutionContext>
{
  public var text: String?

  public func execute() { }
}

public protocol EventArgs { }

public class Event<senderT, argsT : EventArgs>
{
  private let sender: senderT

  public init(sender: senderT)
  {
    self.sender = sender
  }

  public func invoke(with args: argsT)
  {

  }
}

public struct CommandChangedEventArgs<identifierT : StringEnumType> : EventArgs
{
  public let commandIdentifier: identifierT

  public let keyPath: AnyKeyPath
}

public typealias CommandChangedEvent<subjectT,
                                     identifierT : StringEnumType,
                                     executionContextT : ExecutionContext,
                                     commandT : Command<subjectT, identifierT, executionContextT>> = Event<CommandSet<subjectT, identifierT, executionContextT, commandT>, CommandChangedEventArgs<identifierT>>

public protocol NotifyCommandChanged
{
  associatedtype SubjectType

  associatedtype IdentifierType : StringEnumType

  associatedtype ExecutionContextType : ExecutionContext

  associatedtype CommandType : Command<SubjectType, IdentifierType, ExecutionContextType>

  var commandChanged: CommandChangedEvent<SubjectType, IdentifierType, ExecutionContextType, CommandType> { get set }
}

open class CommandSet<subjectT,
                      commandIdentifierT : StringEnumType,
                      executionContextT : ExecutionContext,
                      commandT : Command<subjectT, commandIdentifierT, executionContextT>> : NotifyCommandChanged
{
  private lazy var commands: [commandIdentifierT : commandT] = .init()

  public lazy var commandChanged: CommandChangedEvent<subjectT, commandIdentifierT, executionContextT, commandT> = .init(sender: self)
}

So, if you want minimal changes, this works for me:

open class CommandSet<SubjectType,
  IdentifierType : StringEnumType,
  ExecutionContextType : ExecutionContext,
  CommandType : Command<SubjectType, IdentifierType, ExecutionContextType>> : NotifyCommandChanged
{
  private lazy var commands: [IdentifierType: CommandType] = .init()

  public lazy var commandChanged: CommandChangedEvent<SubjectType, IdentifierType, ExecutionContextType, CommandType> = .init(sender: self)
}

The compiler seems to be struggling to match the associated types.

However, I find the code you provided to be quite difficult to follow. You repeat constraints quite a lot, and lots of the time they are just redundant. For example in CommandSet - if the commandT generic parameter already contains knowledge of the subject/identifier/execution context, you shouldn't need to repeat that every time.

I restructured it quickly to use a protocol instead. Maybe consider something like this:

public protocol StringEnumType : Hashable, RawRepresentable where Self.RawValue == String { }

public protocol ExecutionContext { }

// This is new.
public protocol CommandProtocol: AnyObject {
  associatedtype SubjectType
  associatedtype IdentifierType: StringEnumType
  associatedtype ExecutionContextType: ExecutionContext

  var text: String? { get }
  func execute()
}

// This might not be needed any more. 
// Depending on the rest of your code, maybe you can leave it as a protocol.
open class BaseCommand<subjectT, identifierT : StringEnumType, executionContextT : ExecutionContext>: CommandProtocol
{
  public typealias SubjectType = subjectT
  public typealias IdentifierType = identifierT
  public typealias ExecutionContextType = executionContextT

  public var text: String?

  public func execute() { }
}

public protocol EventArgs { }

public class Event<senderT, argsT : EventArgs>
{
  private let sender: senderT

  public init(sender: senderT)
  {
    self.sender = sender
  }

  public func invoke(with args: argsT)
  {

  }
}

public struct CommandChangedEventArgs<identifierT : StringEnumType> : EventArgs
{
  public let commandIdentifier: identifierT

  public let keyPath: AnyKeyPath
}

public typealias CommandChangedEvent<C: CommandProtocol>
   = Event<CommandSet<C>, CommandChangedEventArgs<C.IdentifierType>>

public protocol NotifyCommandChanged
{
  associatedtype CommandType : CommandProtocol

  var commandChanged: CommandChangedEvent<CommandType> { get set }
}

open class CommandSet<C : CommandProtocol> : NotifyCommandChanged
{
  private lazy var commands: [C.IdentifierType : C] = .init()

  public lazy var commandChanged: CommandChangedEvent<C> = .init(sender: self)
}

Thank you so much Karl. It was how to use the associated types from the protocol directly in the class parameters that solved it. I am so used to "proper" generic protocols in C# and, even after years working with Swift generics, there are still some hints and wrinkles for dealing with PAT protocols that I have not yet come across.

Of course, as expected, refactoring for generics is causing all sorts of fun and games, especially when it comes to integrating all this wonderful stuff with the Objective-C limitations of the Interface Builder facing parts of the framework :roll_eyes::open_mouth::heart_eyes::kissing_heart:

Terms of Service

Privacy Policy

Cookie Policy