How to get a class with generic type accept an array of different by same generic types?
Asked Answered
C

3

1

I'm in the process trying to learn and understand protocols with associated types in swift.

At the same time, I'm learning SwiftUI and taking a course on Udemy.

The app that we will building is a coffee order application.

With that being said, I do not follow the tutorial to the "T" as I tried to structure the app differently, so I can learn to think on my own. The app is nothing too fancy.

The tutorial doesn't use generics and protocols to represent or structure data. It is just a tutorial to showcase SwiftUI.

I created a protocol called Coffee that has a CupSize associated type.

Each coffee Cappuccino, Espresso, and Brewed Coffee confirms to the Coffee protocol.

protocol Priceable {
  var cost: Double { get }
}

protocol Coffee {
  associatedtype CupSize
  var cupSize: CupSize { get }
  init(cupSize: CupSize)
}

enum EspressoCupSize {
  case small
}

struct Espresso: Coffee, Priceable {
  var cupSize = EspressoCupSize.small
  var cost: Double { return 3.00 }
}

enum BrewedCoffeeCupSize {
  case small
  case medium
  case large
}

struct BrewedCoffee: Coffee, Priceable {
  var cupSize: BrewedCoffeeCupSize
  var cost: Double {
    switch self.cupSize {
      case .small: return 1.00
      case .medium: return 2.00
      case .large: return 3.00
    }
  }
}

enum CappuccinoCupSize {
  case small
  case medium
  case large
}

struct Cappuccino: Coffee, Priceable {
  var cupSize: CappuccinoCupSize
  var cost: Double {
    switch self.cupSize {
      case .small: return 2.00
      case .medium: return 3.00
      case .large: return 4.00
    }
  }
}

Then, I created an Order struct and an OrderManager class.

Order struct has a generic and needs to be a Priceable item. The idea of a generic priceable item is to support other items in the future in case I want to expand the app...not just coffee.

The idea behind OrderManager is to keep track all the orders and manage the CRUD operations of the orders (still need to implement delete, read, and update).

struct Order<Item: Priceable> {
  var name: String
  var item: Item
}

class OrderManager<Item> {
  private var orders: [Item] 

  init(orders: [Item]) {
    self.orders = orders
  }

  func add(_ order: Item) {
    self.orders.append(order)
  } 
}

My issue is using OrderManager.

let maryOrder = Order(name: "Mary", item: Espresso())
let sueOrder = Order(name: "Sue", item: BrewedCoffee(cupSize: .medium))

// Dummy Structure
struct Person {}

let orderManager = OrderManager(orders: [
  maryOrder,
  sueOrder,
  Person() // This works!!! Which is not what I want.
])

I want the generic type for OrderManager to be an Order, but since Order has its own generic type of Priceable, I cannot seem to find the correct answer or find the correct syntax.

Things I have tried to get OrderManager to work

class OrderManager<Order> {} // Does not work because Order needs a generic type
class OrderManager<Order<Priceable>> // Still does not work
class OrderManager<Item: Priceable, Order<Item> {} // Still does not work.
// and I tried other solutions, but I cannot get this to work
// Also, when I think I got the right syntax, I cannot add Mary and Sue's orders to
// OrderManager because Mary's item is Espresso and Sue's item is BrewedCoffee

How can I get OrderManager to accept only an array of orders?

Chela answered 26/10, 2019 at 11:42 Comment(0)
E
3

It's good that you want to experiment with generics, but this isn't the occasion for it. You say:

The idea ... is to support other items in the future in case I want to expand the app...not just coffee.

But you don't need a generic for that. The only requirement for managing an order is that the order's item be Priceable. Priceable is already a type; you don't need to add a generic type to the mix.

struct Order {
  var name: String
  var item: Priceable
}

class OrderManager {
    private var orders: [Order]

  init(orders: [Order]) {
    self.orders = orders
  }

  func add(_ order: Order) {
    self.orders.append(order)
  }
}
Ephesus answered 26/10, 2019 at 14:6 Comment(1)
Geez, I feel like an idiot! I can't believe I was so hung up on generics that I totally forgot to use Priceable as the item. Thank you so much for the response.Chela
L
2

I'm in the process trying to learn and understand generics with associated types in swift.

There is no such thing as "generics with associated types in Swift." There are generics, and there are protocols with associated types (PAT). They have some things in common, but are deeply different concepts used for very different things.

The purpose of a generic is to allow the type to vary over types chosen by the caller.

The purpose of a PAT is to allow a type to be used by existing algorithms, using types chosen by the implementer. Given this, Coffee does not make sense as a protocol. You're trying to treat it like a heterogeneous type. That's not what a PAT is. A PAT is a hook to allow types to be used by algorithms.

class OrderManager<Item> { ... }

This says that OrderManager can hold anything; literally anything at all. It doesn't have to be Priceable. In your case, Item is being coerced into Any, which is definitely not what you wanted (and why Person works when it shouldn't). But it doesn't make a lot of sense that OrderManager is tied to some item type. Do you really want one OrderManager for Coffee and a completely different OrderManager for Espresso? That doesn't match what you're doing at all. OrderManager should work over an Order of anything, right?

It's not really possible to determine what protocols and generics you do need here because you never do anything with OrderManager.orders. Start with the calling code. Start with no generics or protocols. Just let the code duplicate, and then extract that duplication into generics and protocols. If you don't have a clear algorithm (use case) in mind, you should not be creating a protocol yet.

See matt's answer for a starting point, but I'm sure it's not enough for your problem. You likely will need more things (most likely the name of the item for instance). Start with some simple structs (Espresso, BrewedCoffee, etc), and then start working out your calling code, and then you'll probably have more questions we can discuss.


To your question of how to attack this kind of problem, I would begin like this.

First, we have some items for sale. I model them in their most obvious ways:

// An Espresso has no distinguishing characteristics.
struct Espresso {}

// But other coffees have a size.
enum CoffeeSize: String {
    case small, medium, large
}

// You must know the size in order to create a coffee. You don't need to know
// its price, or its name, or anything else. But you do have to know its size
// or you can't pour one. So "size" is a property of the type.
struct BrewedCoffee {
    let size: CoffeeSize
}

struct Cappuccino {
    let size: CoffeeSize
}

Done!

OK, not really done, but seriously, kind of done. We can now make coffee drinks. Until you have some other problem to solve, you really are done. But we do have another problem:

We want to construct an Order, so we can give the customer a bill. An Order is made up of Items. And Items have names and prices. New things can be added to an Order, and I can get textual representation of the whole thing. So we model first what we need:

struct Order {
    private (set) var items: [Item]
    mutating func add(_ item: Item) {
        items.append(item)
    }

    var totalPrice: Decimal { items.map { $0.price }.reduce(0, +) }
    var text: String { items.map { "\($0.name)\t\($0.price)" }.joined(separator: "\n") }
}

And to implement that, we need a protocol that provides name and price:

protocol Item {
    var name: String { get }
    var price: Decimal { get }
}

Now we'd like an Espresso to be an Item. So we apply retroactive modeling to make it one:

extension Espresso: Item {
    var name: String { "Espresso" }
    var price: Decimal { 3.00 }
}

And the same thing with BrewedCoffee:

extension BrewedCoffee {
    var name: String { "\(size.rawValue.capitalized) Coffee" }
    var price: Decimal {
        switch size {
        case .small: return 1.00
        case .medium: return 2.00
        case .large: return 3.00
        }
    }
}

And of course Cappuccino...but you know, as I start to write that I really want to cut-and-paste BrewedCoffee. That suggests maybe there's a protocol hiding in there.

// Just a helper to make syntax prettier.
struct PriceMap {
    var small: Decimal
    var medium: Decimal
    var large: Decimal
}

protocol SizedCoffeeItem: Item {
    var size: CoffeeSize { get }
    var baseName: String { get }
    var priceMap: PriceMap { get }
}

With that, we can implement the requirements of Item:

extension SizedCoffeeItem {
    var name: String { "\(size.rawValue.capitalized) \(baseName)" }
    var price: Decimal {
        switch size {
        case .small: return priceMap.small
        case .medium: return priceMap.medium
        case .large: return priceMap.large
        }
    }
}

And now the conformances require no code duplication.

extension BrewedCoffee: SizedCoffeeItem {
    var baseName: String { "Coffee" }
    var priceMap: PriceMap { PriceMap(small: 1.00, medium: 2.00, large: 3.00) }
}

extension Cappuccino: SizedCoffeeItem {
    var baseName: String { "Cappuccino" }
    var priceMap: PriceMap { PriceMap(small: 2.00, medium: 3.00, large: 4.00) }
}

These two examples are of two different uses of protocols. The first is implementing a heterogeneous collection ([Item]). These kinds of protocols cannot have associated types. The second is to facilitate code sharing between types. These kinds can. But in both cases I didn't add any protocols until I had a clear use case: I needed to be able to add them to Order and get back certain kinds of data. That led us to each step along the way.

As a starting point, model your data simply and design your algorithms. Then adapt your data to the algorithms with protocols. Protocols come late, not early.

Lassitude answered 26/10, 2019 at 18:17 Comment(6)
You are absolutely right. What I meant is PAT (protocols with associated types) and not generics with associated types. I'm not sure why I said generics with associated types in the first place :). "The purpose of a generic is to allow the type to vary over types chosen by the caller." -- I would assume library is a good example? Actually, I was going to use Coffee as the parent class and the others as sub classes. However, I keep reading stuff like start with a struct and use composition over inheritance. I tried to force myself to learn and use POP instead of OOP. Thanks for the response.Chela
Update post to say protocols with associated types.Chela
"I would assume library is a good example." No; being in a library or not has nothing to do with this. It has to do with whether the types are chosen by the caller (generics) or the implementation (PAT). Just converting class-based inheritance into protocols leads to very bad protocols. PATs are not about inheritance; they do not replace classes. They are a way of solving a different kind of code reuse problem (the application of generic algorithms to types). If you don't have an algorithm that needs to work on multiple unrelated types, then you don't want a PAT.Lassitude
Thanks for the reply. This might be far fetched, but how would you structure the code in this case above? Some examples would be great to lear from.Chela
LOL...I didn't see you modified your content. Thanks for tasking the time to show some examples. I will study and look over them. Thank you so much!Chela
I didn't like the fact that I duplicate the size and was going to solve it later, but it looks like you beat me to it :)Chela
G
1

You do not need to define Generic type on the class , you should put it on a method like following:

class OrderManager {
   func doOrder<T: Priceable, L: Protocol2 >(obj: T) -> L {
     // Use obj as a Priceable model
     // return a Protocol2 model
   }
}

and for send to the class you just send your model for example

varProtocol2 = OrderManager.doOrder(obj: maryOrder.item)

Here is an example with two generic object

protocol prot1 {
    var a: Int {get set}
}

protocol protRet {
    var b: String {get set}
    init()
}

struct prot1Struct: prot1 {
    var a: Int
}

struct prot2Struct: protRet {
    init() {
        b = ""
    }
    var b: String
}

class Manage {
    func Do<T: prot1, L: protRet>(obj: T) -> L {
        var ret: L = L()
        ret.b = "\(obj.a)"
        return ret
    }
}


var obj1: prot2Struct?
var paramItem = prot1Struct(a: 10)
obj1 = Manage().Do(obj: paramItem)

Also if you want to use it on a Class you can do it like following:

class manageb<T: prot1, L: protRet> {
    func Do(obj: T) -> L {
        var ret: L = L()
        ret.b = "\(obj.a)"
        return ret
    }
}

var obj1: prot2Struct?
var paramItem = prot1Struct(a: 10)

let classB = manageb<prot1Struct, prot2Struct>()
obj1 = classB.Do(obj: paramItem)
Gen answered 26/10, 2019 at 13:40 Comment(2)
Thank so much for your response! Even though I accepted and going to use Matt's answer, I'm going to use your code to experiment a bit.Chela
@Chela Ok, let’s upvote if it’s useful to youGen

© 2022 - 2024 — McMap. All rights reserved.