Swift: Protocol Based Type Construction

523 views Asked by At

I'm trying to create a protocol in Swift I can use for object construction. The problem I'm running into is that I need to store the type information so the type can be constructed later and returned in a callback. I can't seem to find a way to store it without either crashing the compiler or creating build errors. Here's the basics (a contrived, but working example):

protocol Model {
  init(values: [String])
  func printValues()
}

struct Request<T:Model> {
  let returnType:T.Type
  let callback:T -> ()
}

We have a simple protocol that declares a init (for construction) and another func printValues() (for testing). We also define a struct we can use to store the type information and a callback to return the new type when its constructed.

Next we create a constructor:

class Constructor {
  var callbacks: [Request<Model>] = []

  func construct<T:Model>(type:T.Type, callback: T -> ()) {
    callback(type(values: ["value1", "value2"]))
  }

  func queueRequest<T:Model>(request: Request<T>) {
    callbacks.append(request)
  }

  func next() {
    if let request = callbacks.first {
      let model = request.returnType(values: ["value1", "value2"])
      request.callback(model)
    }
  }
}

A couple things to note: This causes a compiler crash. It can't figure this out for some reason. The problem appears to be var callbacks: [Request<Model>] = []. If I comment out everything else, the compiler still crashes. Commenting out the var callbacks and the compiler stops crashing.

Also, the func construct works fine. But it doesn't store the type information so it's not so useful to me. I put in there for demonstration.

I found I could prevent the compiler from crashing if I remove the protocol requirement from the Request struct: struct Request<T>. In this case everything works and compiles but I still need to comment out let model = request.returnType(values: ["value1", "value2"]) in func next(). That is also causing a compiler crash.

Here's a usage example:

func construct() {
  let constructor = Constructor()
  let request = Request(returnType: TypeA.self) { req in req.printValues() }

  //This works fine
  constructor.construct(TypeA.self) { a in
    a.printValues()
  }

  //This is what I want
  constructor.queueRequest(request)
  constructor.next() //The callback in the request object should be called and the values should print
}

Does anyone know how I can store type information restricted to a specific protocol to the type can later be constructed dynamically and returned in a callback?

4

There are 4 answers

4
Qbyte On BEST ANSWER

If you want the exact same behavior of next I would suggest to do this:

class Constructor {
  // store closures
  var callbacks: [[String] -> ()] = []

  func construct<T:Model>(type:T.Type, callback: T -> ()) {
    callback(type(values: ["value1", "value2"]))
  }

  func queueRequest<T:Model>(request: Request<T>) {
    // some code from the next function so you don't need to store the generic type itself
    // **EDIT** changed closure to type [String] -> () in order to call it with different values
    callbacks.append({ values in
      let model = request.returnType(values: values)
      request.callback(model)
    })
  }

  func next(values: [String]) {
    callbacks.first?(values)
  }
}

Now you can call next with your values. Hopefully this works for you.

EDIT: Made some changes to the closure type and the next function

1
Qbyte On

Unfortunately there is no way to save specific generic types in an array and dynamically call their methods because Swift is a static typed language (and Array has to have unambiguous types).

But hopefully we can express something like this in the future like so:

var callbacks: [Request<T: Model>] = []

Where T could be anything but has to conform to Model for example.

1
AnthonyMDev On

Your queueRequest method shouldn't have to know the generic type the Request it's being passed. Since callbacks is an array of Request<Model> types, the method just needs to know that the request being queued is of the type Request<Model>. It doesn't matter what the generic type is.

This code builds for me in a Playground:

class Constructor {
  var callbacks: [Request<Model>] = []

  func construct<T:Model>(type:T.Type, callback: T -> ()) {
    callback(type(values: ["value1", "value2"]))
  }

  func queueRequest(request: Request<Model>) {
    callbacks.append(request)
  }

  func next() {
    if let request = callbacks.first {
      let model = request.returnType(values: ["value1", "value2"])
      request.callback(model)
    }
  }
}
0
Aaron Hayman On

So I found an answer that seems to do exactly what I want. I haven't confirmed this works yet in live code, but it does compile without any errors. Turns out, I needed to add one more level of redirection:

I create another protocol explicitly for object construction:

protocol ModelConstructor {
  func constructWith(values:[String])
}

In my Request struct, I conform to this protocol:

struct Request<T:Model> : ModelConstructor {
  let returnType:T.Type
  let callback:T -> ()

  func constructWith(values:[String]) {
    let model = returnType(values: values)
    callback(model)
  }
}

Notice the actual construction is moved into the Request struct. Technically, the Constructor is no longer constructing, but for now I leave its name alone. I can now store the Request struct as ModelConstructor and correctly queue Requests:

class Constructor {
  var callbacks: [ModelConstructor] = []

  func queueRequest(request: Request<Model>) {
    queueRequest(request)
  }

  func queueRequest(request: ModelConstructor) {
    callbacks.append(request)
  }

  func next() {
    if let request = callbacks.first {
      request.constructWith(["value1", "value2"])
      callbacks.removeAtIndex(0)
    }
  }
}

Note something special here: I can now successfully "queue" (or store in an array) Request<Model>, but I must do so indirectly by calling queueRequest(request: ModelConstructor). In this case, I'm overloading but that's not necessary. What matters here is that if I try to call callbacks.append(request) in the queueRequest(request: Request<Model>) function, the Swift compiler crashes. Apparently we need to hold the compiler's hand here a little so it can understand what exactly we want.

What I've found is that you cannot separate Type information from Type Construction. It needs to be all in the same place (in this case it's the Request struct). But so long as you keep construction coupled with the Type information, you're free to delay/store the construction until you have the information you need to actually construct the object.