4

I am trying to make Data Model for my app. here is the scenario:

my app has Customer Model which contains customer's info, and also contain his/her Payment Source. the API gives me two kind of payment sources: card and bank account which they have completely different fields.

So, here is my problem, I want to have abstract type which is PaymentSource then within each PaymentSource have a function to return object casted to it's type. some how I am type erasure.

I needed to put my abstract type in a box and use it as Concrete type (AnyPaymentSource).

So, I've done as following:

protocol PaymentSource {
    associatedtype Kind
    func cast() -> Kind
}

struct AnyPaymentSource<PS: PaymentSource> {
    private var paymentSource: PS
    init(paymentSource: PS) {
        self.paymentSource = paymentSource
    }
    func cast() -> PS.Kind {
        return paymentSource.cast()
    }
}

struct Card: PaymentSource {
    func cast() -> Card {
        return self
    }
}

struct BankAccount: PaymentSource {
    func cast() -> BankAccount {
        return self
    }
}

struct Customer { 
    var firstName: String
    var lastName: String
    var email: String
    var paymentSource : AnyPaymentSource<PaymentSource> 
}

but Customer gives me error with following description:

Using 'PaymentSource' as a concrete type conforming to protocol 'PaymentSource' is not supported

where am I doing wrong?

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
Mohammadalijf
  • 1,387
  • 9
  • 19
  • 1
    The generic type `PS` must be a concrete type like `BankAccount` or `Card` rather than the protocol it conforms to. – vadian Apr 12 '17 at 08:46
  • @vadian then why bother create abstract type for customer's PaymentSource? i need to store payment sources without knowing what type it is. then later in code get the type – Mohammadalijf Apr 12 '17 at 09:15
  • What is the purpose of these `PaymentSource` structs? As it is, you're better off going for an `enum` for the two types of payment sources. – XmasRights Apr 12 '17 at 09:20
  • @XmasRights true i can use enums with Generics but then later i have to use lots of switch cases. i am looking for more elegant solution like associatedtypes – Mohammadalijf Apr 12 '17 at 09:22

2 Answers2

4

Swift is statically typed language. That means the type of a variable must be known at compile time.

When i was faced with this problem, i solved it something like this

protocol PaymentSource {
    associatedtype Kind
    func cast() -> Kind
}

struct AnyPaymentSource<PS: PaymentSource> {
    private var paymentSource: PS
    init(paymentSource: PS) {
        self.paymentSource = paymentSource
    }
    func cast() -> PS.Kind {
        return paymentSource.cast()
    }
}

struct Card: PaymentSource {
    func cast() -> Card {
        return self
    }
}

struct BankAccount: PaymentSource {
    func cast() -> BankAccount {
        return self
    }
}

struct Customer<T:PaymentSource> {
    var firstName: String
    var lastName: String
    var email: String
    var paymentSource : AnyPaymentSource<T>
}
func test(){
    let customerWithCard = Customer<Card>(
        firstName: "",
        lastName: "",
        email: "",
        paymentSource: AnyPaymentSource(paymentSource: Card())
    )
    let customerWithBankAccount = Customer<BankAccount>(
        firstName: "",
        lastName: "",
        email: "",
        paymentSource: AnyPaymentSource(paymentSource: BankAccount())
    )
    print(customerWithCard.paymentSource.cast())
    print(customerWithBankAccount.paymentSource.cast())
    return
}
Community
  • 1
  • 1
0x384c0
  • 2,256
  • 1
  • 17
  • 14
  • 1
    aaw thank you, i am going to mix your answer with stuff on this [blog](https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/) – Mohammadalijf Apr 12 '17 at 09:38
  • 1
    For me, I think that there is a "code smell" here. mentioning that `cardCustomer = Customer(...` and also `AnyPaymentSource(paymentSource: Card())` doesn't seems to be alright, you are mentioning the same thing here – Ahmad F Apr 12 '17 at 09:42
  • 1
    @AhmadF this is called Type Erasure which is even used in swift standard library (see [here](https://github.com/apple/swift/blob/2fe4254cb712fa101a220f95b6ade8f99f43dc74/stdlib/public/core/ExistentialCollection.swift.gyb#L45)) also more example you can check [here](https://www.natashatherobot.com/swift-type-erasure/) and [here](https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/) – Mohammadalijf Apr 12 '17 at 11:06
0

If what are you trying to achieve is what @Andrew Ashurov mentioned in his answer, there is no need to implement AnyPaymentSource. As mentioned in Swift Protocols Documentation:

Protocols do not actually implement any functionality themselves. Nonetheless, any protocol you create will become a fully-fledged type for use in your code.

Meaning that are already able to treat a protocol as a type.

It might be:

protocol PaymentSource {
    func cast() -> Self
}

struct Card: PaymentSource {
    func cast() -> Card {
        return self
    }
}

struct BankAccount: PaymentSource {
    func cast() -> BankAccount {
        return self
    }
}

struct Customer {
    var firstName: String
    var lastName: String
    var email: String
    var paymentSource : PaymentSource?
}

Creating Customers:

let cardCustomer = Customer(firstName: "Card Fname", lastName: "Card Lname", email: "cardemail@example.com", paymentSource: Card())

let bankAccountCustomer = Customer(firstName: "Bank Account Fname", lastName: "Bank Account Lname", email: "bankaccountemail@example.com", paymentSource: BankAccount())

Note that in Customer struct, paymentSource property of type PaymentSource which means it can assigned as any type that conforms to PaymentSource protocol (Card and BankAccount in your case).

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
  • 1
    when you call cast on paymentSource in customer it will return you a PaymentSource. with associatedtype i can specify which Type it should cast to. read this [blog](https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/) for detailed problem and solution – Mohammadalijf Apr 12 '17 at 11:04
  • I think you didn't get the question exactly. The question is how to return the exact type, the object Customer still treats the `paymentSource` property as protocol, you cannot do it the way you suggested without any `associatedTypes`... That is exactly what they were meant for. – Dominik Bucher Mar 23 '22 at 00:58