Overcoming Cyclic Generic Parameters

Having just spent a couple of frustrating days battling with a complex framework with lots of generics involved, I had stumbled across a need to have two generic classes that referred to each other; and, yes, it was very necessary due to strict type checking requirements when the framework gets used outside of the framework.

What I had originally was :

protocol CommandProtocol
{
  init()
}

class Command<modelT> : CommandProtocol
{
  required init() { }
}

class Model<subjectT, commandT : CommandProtocol>
{
  var subject: subjectT

  init(subject: subjectT)
  {
    self.subject = subject
  }
  
  // hoping this would avoid having to declare a strictly type command in each model
  lazy var command: commandT = .init()
}

// This is where it gets recursive but the compiler doesn't give any errors or even a warning
class StringModelCommand : Command<StringModel>
{
  
}

class StringModel : Model<String, StringModelCommand>
{

}

It all compile fine, without even a warning but, when you run :

  {
    let model = StringModel(subject: "Fred")
  }

Kaboom !!!

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

Now, I knew this was likely to be a problem but was surprised when the compiler didn't give me any indication of a possible runtime crash. So I set about finding the best (and neatest) way of sorting this problem.

Here's the resulting code from a long day's effort :

protocol CommandProtocol
{
  associatedtype ModelType : ModelProtocol
  
  init()
}

protocol ModelProtocol
{
  associatedtype CommandType : CommandProtocol
  
  var command: CommandType { get }
}

class Command<modelT : ModelProtocol> : CommandProtocol
{
  typealias ModelType = modelT
  
  required init() { }
}

class Model<subjectT>
{
  var subject: subjectT
  
  init(subject: subjectT)
  {
    self.subject = subject
  }
}

class StringCommand : Command<StringModel>
{

}

class StringModel : Model<String>, ModelProtocol
{
  lazy var command: StringCommand = .init()
}

So, instead of parameterising the model with the command type, all that is needed is to implement the ModelProtocol and implement the command var in each derived model.

Phew ! I hope this helps someone :sunglasses:

I do not replicate this. When I copy and paste your code into an Xcode playground, it compiles and runs with no error and no crash.

I will also note that this is not the first time you have started a thread with a code sample that you claim either crashes or fails to compile, when in fact it works properly.

Also, is there a reason you are defying Swift’s recommended practice of starting type names (in this case, generic placeholders) with uppercase letters?

1 Like

Well, I am using Xcode 10.1 with Swift 4.2 and, if you think I would go to all the trouble of posting something that wasn't causing a genuine problem, then you don't appreciate how long I have been teaching others about programming (just under 30 years)

Also, I have my own convention for generic parameter names, ensuring that, when I am implementing a protocol with associated types (which do start with uppercase), it is easier to not get confused. But then, with my almost thirty years of teaching programming, I would question whether your reply isn't a case of "teaching your grandmother how to suck eggs". Now, how many years have you been programming?

P.S., it also crashes in a playground in my version of Xcode.

I‘d like to step in here and kindly ask not to go any further such kind of a discussion here on forums. I don‘t think this is appropriate. Nevin from my perspective asked you why you chose to write Swift code other than the majority of the community 'likely' does by following the swift naming guidelines and a modern style, while modern style is just like iOS 7 vs. iOS 6 from the visual perspective. It‘s your right to write code however you like, but it‘s fair to say that most people would appreciate to speak the same language and dialect with each other without questioning how long the conversation partner lived on earth or learned a language.

(This is my personal opinion and might not reflect what Nevin thinks, but I find it a little offensive to be stamped as a noob, kindly meaning, just because I didn‘t do something for a specific period of time. That said I think that time does not reflect professionalism.)

2 Likes

Hi Adrian. Yes, you're right but, after two long intensive days battling with this, I would have picked a fight with the dog, if only I had one.

My apologies. Do you want me to delete my post?

No, I‘m also no trying to play any forums police here. My intention was to cool down the conversation to an appropriate degree. ;)

I‘m sorry for your trouble, I‘ve been there myself, Swift isn‘t perfect yet or may not ever be, but the community might be able to help you with your issue. :slight_smile: Also keep in mind if playground crashes, it does not mean that the main program will also crash in real project, playground was and still is not that stable as an ordinary target.

Which is why I tend to use Xcode in a test project rather than risk a playground. Of course, the compiler may have changed since 4.2 but, until I can find the time to install on a newer Mac which supports Mojave, I am where I am :wink:

I just had a closer look at the generic types (not the members), I think the reason why the compiler allowed is because it wasn‘t smart enough to catch the complexity of this pattern. I also think this smells like a derivation of CRTP which can be mind bending but still valid. Take this valid Swift 5 for example.

class G<T> {
  class S: G<S>
    var t: T
    init(_ t: T) {
      self.t = t
    }
  }
}

let a = G<Int>.S(42)
let b: G<G<Int>.S> = a // works but mind bending at first glance

I‘m on my iPad so I can‘t test your original example in Swift 5. CRTP was mostly implemented with Swift 5. I said mostly, because there is a potential CRTP pattern in protocols, which is currently rejected by the compiler.

Indeed, one of the things I tried to do a while back was port parts of a framework I wrote in C#, over several years, for a very large client, which had a great deal of CRTP in it. It worked (and still works) wonderfully, as the undergirding for a massive business management package.

For example :

  public abstract class CommandSet<commandSetT, commandT> : ICommandSet,
                                                            IEnumerable<commandT>
                                                            where commandSetT : CommandSet<commandSetT, commandT>
                                                            where commandT : Command
  {

And, for a fully bound class, we got stuff like this :

  public partial class BankAccount : BaseObject<BankAccount>
  {

I still have to blink several times at the apparent complexity of the declarations, but by the time it was derived from in a concrete class, with all the parameters bound, things became a lot easier to follow.

I'm obviously going to have to get my newer machine up and running with Swift 5 if you say this should now work. Although, I must admit to being pretty proud of myself having circumvented the problem in 4.2 and actually quite like the "simpler" syntax I got out of it.

It's always helpful when talking about unexpected crashes (whether at compile time or run time) to mention what version of Swift you're using. You're more than welcome to talk about your experience using older tools, but it's likely that most people here will assume by default that you're using the latest release, at least after it's been out for more than a few weeks.

In this case, yes, we've absolutely fixed some problems with cyclic generics in Swift 5. (It was one of the things we felt we needed to get right before we could declare a stable ABI.)

As for the tone problems in this thread, while I think Joanna reacted a little defensively, I can also understand why she felt called out by Nevin's reply. An unaccounted-for configuration difference is almost always the cause of a failure to replicate a problem; please try to remember to give each other some credit.

9 Likes

Well, apart from the misunderstanding, I am extremely pleased to say that I now have a fully functioning MVP framework that is relatively easy to derive from. Thanks to all here for their insights.

2 Likes