0

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?

Aaron Hayman
  • 8,492
  • 2
  • 36
  • 63

4 Answers4

1

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

Qbyte
  • 12,753
  • 4
  • 41
  • 57
  • So you do have a good idea here and it technically works, but it defeats the purpose of storing the callback in the first place. The idea is that I'm not going to immediately have the values to construct the model with. Once I do have those values, then I construct the model and return it in the callback. I can't do that in this approach. However, +1 for finding a workaround even if I can't use it. – Aaron Hayman Jun 23 '15 at 03:19
  • Thank you for your comment. I made some changes to my answer. I hope this helps :) – Qbyte Jun 23 '15 at 11:06
  • Yep, this works. It's a clever approach, using the closure to appropriately capture the Type information. I also found another approach that works by moving the actual construction into the `Request` object. I'm not sure which approach is better, probably depends on the context, but I'm marking yours as the answer as it requires less code than my approach. – Aaron Hayman Jun 23 '15 at 14:11
  • Great! Your approach could be better (also depending on the context) because you encapsulate the construction inside a struct rather than a closure which is a first class type. It could also be easier to handle an array of structs due to their value type behavior. – Qbyte Jun 23 '15 at 14:49
0

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.

Qbyte
  • 12,753
  • 4
  • 41
  • 57
  • So I've found I can store the `Request` object without issue if I use `var callbacks: [Request]` but remove the type info `` in the `queueRequest` so that it simply passes `Request`. So I can *store* the Request object, but I still can't *use* the Request object. The problem isn't storing `Request` in an array, the problem is that I can't capture the necessary Type information in the `Request` struct. But given that the `func construct...` works correctly, it seems that there's gotta be a way to store and then use protocol type information for instantiation. – Aaron Hayman Jun 19 '15 at 13:27
0

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)
    }
  }
}
AnthonyMDev
  • 1,496
  • 1
  • 13
  • 27
  • 1
    You're right, in that doing so no longer crashes when *storing* the `Request` object, but it still won't compile in the `next()` function on `let model = request.returnType(values: ["value1", "value2"])` (Xcode 6.3 throws `segment fault 11`, Xcode 7 Beta 1 throws `illegal instruction: 4`). Playgrounds are useful, but they don't always show you compiler errors. I recommend creating a small blank project with this code and try to build instead. – Aaron Hayman Jun 19 '15 at 13:17
0

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.

Aaron Hayman
  • 8,492
  • 2
  • 36
  • 63