Reputation: 493
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?
Upvotes: 1
Views: 666
Reputation: 299703
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.
Upvotes: 2
Reputation: 536057
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)
}
}
Upvotes: 3
Reputation: 5088
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)
Upvotes: 1