Swift Protocol That Is Using an Enum with Generic Associated Type

Swift protocol that is using an enum with generic associated type

Here’s the problem: imagine some subsequent lines of code.

// none of this will compile...
var bar = [AssociatedProtocol]()
bar.append(GenericEnum.Associated(1))
bar.append(GenericEnum.Associated("hello")
let foo = bar[0].foo()

What type is foo? Is it a GenericEnum<Int> or a GenericEnum<String>? Or neither?

This is especially a problem because enums, like structs, are “value types”. That means their size is determined by what they contain. Take the following code:

let x = GenericEnum.Associated(1)
sizeofValue(x) // 9 - 1 byte for the enum, 8 for the Int
let y = GenericEnum.Associated("hello")
sizeofValue(y) // 25 - 1 byte for the enum, 24 for the String

Protocols with associated types are only really there to constrain generic functions. So this would be fine:

func f<T: AssociatedProtocol>(values: [T]) {
var bar = [T]() // T is an instance of a specific
// AssociatedProtocol where T.AssociatedType
// is fixed to some specific type
}

but to use it stand-alone doesn’t make sense (at least with the current version 1.2 of Swift – new features might enable other things in the version).

If you need the protocol to be used polymorphically dynamically at runtime, you would need to ditch the typealias. Then instead it can be used as a fixed-size reference.

enums with Associated Values + generics + protocol with associatedtype

I'm trying to make my API Service as generic as possible:

First, and most importantly, this should never be a goal. Instead, you should start with use cases, and make sure that your API Service meets them. "As generic as possible" doesn't mean anything, and only will get you into type nightmares as you add "generic features" to things, which is not the same thing as being generally useful to many use cases. What callers require this flexibility? Start with the callers, and the protocols will follow.

func send<T>(request: RestRequest) -> T

Next, this is a very bad signature. You don't want type inference on return types. It's a nightmare to manage. Instead, the standard way to do this in Swift is:

func send<ResultType>(request: RestRequest, returning: ResultType.type) -> ResultType

By passing the expected result type as a parameter, you get rid of the type inference headaches. The headache looks like this:

let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))

How is the compiler to know that stringResponse is supposed to be a String? Nothing here says "String." So instead you have to do this:

let stringResponse: String = ...

And that's very ugly Swift. Instead you probably want (but not really):

let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")),
returning: String.self)

"But not really" because there's no way to implement this well. How can send know how to translate "whatever response I get" into "an unknown type that happens to be called String?" What would that do?

protocol Parseable {
associatedtype ResponseType
func parse() -> ResponseType
}

This PAT (protocol w/ associated type) doesn't really make sense. It says something is parseable if an instance of it can return a ResponseType. But that would be a parser not "something that can be parsed."

For something that can be parsed, you want an init that can take some input and create itself. The best for that is Codable usually, but you could make your own, such as:

protocol Parseable {
init(parsing data: Data) throws
}

But I'd lean towards Codable, or just passing the parsing function (see below).

enum RestRequest {}

This is probably a bad use of enum, especially if what you're looking for is general usability. Every new RestRequest will require updating parse, which is the wrong place for this kind of code. Enums make it easy to add new "things that all instances implement" but hard to add "new kinds of instances." Structs (+ protocols) are the opposite. They make it easy to add new kinds of the protocol, but hard to add new protocol requirements. Requests, especially in a generic system, are the latter kind. You want to add new requests all the time. Enums make that hard.

Is there a better still clean way to achieve this?

It depends on what "this" is. What does your calling code look like? Where does your current system create code duplication that you want to eliminate? What are your use cases? There is no such thing as "as generic as possible." There are just systems that can adapt to use cases along axes they were prepared to handle. Different configuration axes lead to different kinds of polymorphism, and have different trade-offs.

What do you want your calling code to look like?

Just to provide an example of what this might look like, though, it'd be something like this.

final class ApiService {
let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}

func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: @escaping (Response?) -> Void) {
urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
// Log your error
completion(nil)
return
}

if let data = data {
let result = try? JSONDecoder().decode(Response.self, from: data)
// Probably check for nil here and log an error
completion(result)
return
}
// Probably log an error
completion(nil)
}
}
}

This is very generic, and can apply to numerous kinds of use cases (though this particular form is very primitive). You may find it doesn't apply to all your use cases, so you'd begin to expand on it. For example, maybe you don't like using Decodable here. You want a more generic parser. That's fine, make the parser configurable:

func send<Response>(request: URLRequest,
returning: Response.Type,
parsedBy: @escaping (Data) -> Response?,
completion: @escaping (Response?) -> Void) {

urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
// Log your error
completion(nil)
return
}

if let data = data {
let result = parsedBy(data)
// Probably check for nil here and log an error
completion(result)
return
}
// Probably log an error
completion(nil)
}
}

Maybe you want both approaches. That's fine, build one on top of the other:

func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: @escaping (Response?) -> Void) {
send(request: request,
returning: returning,
parsedBy: { try? JSONDecoder().decode(Response.self, from: $0) },
completion: completion)
}

If you're looking for even more on this topic, you may be interested in "Beyond Crusty" which includes a worked-out example of tying together parsers of the kind you're discussing. It's a bit dated, and Swift protocols are more powerful now, but the basic message is unchanged and the foundation of things like parsedBy in this example.

In Swift, how can you use generic enum types in protocol extensions?

There are two different way you could do it depending on what your requirements are.


If you don't require the type to be a enum, you can simply do

protocol AnimalStorageProtocol {
associatedtype AnimalType: Hashable
var storage: [AnimalType: Int] { get set }
}

This will allow any hashable type to be used.


If you require that the types can only be RawRepresentable where the RawValue is a String you'll have to define another protocol that your animal types will have to conform to.

protocol AnimalType: Hashable, RawRepresentable {
var rawValue: String { get }
}

protocol AnimalStorageProtocol {
associatedtype Animal: AnimalType
var storage: [Animal: Int] { get set }
}

Then you just have to set your enum types to conform to the AnimalType protocol.

enum CatType: String, AnimalType { ... }
enum DogType: String, AnimalType { ... }

How to initialize generic enum using a protocol with associated type?

Something like this:

extension Result {
init<R: Response>(response: R) where R.T == T {
if let info = response.info {
self = .success(info: info)
} else {
self = .failure(message: "Whoops")
}
}
}

You can't use protocols-with-associated types directly, so you should use them as generic parameters.

Array of enum types conforming to a protocol with an associated type

I want to display a menu listing all the possible options.

So you want Strings, not the cases themselves. That's absolutely doable. First, start by saying what you really want the type to do in the form of a protocol:

protocol CaseNamed {
static var caseNames: [String]
}

If you had that, you could build what you want:

var enums: [CaseNamed.Type] = [A.self, B.self]
enums.flatMap { $0.caseNames }

(I call this "wish driven development." I wish I had a type that could....)

Now you just need to conform types to CaseNamed by implementing caseNames. Luckily that's easy if the type also happens to conform to CaseIterable:

extension CaseNamed where Self: CaseIterable {
static var caseNames: [String] {
self.allCases.map { "\($0)" }
}
}

But you can have CaseNamed types that don't conform to CaseIterable. CaseIterable isn't a requirement. It's just nice if you have it. Here's the full code:

protocol CaseNamed {
static var caseNames: [String] { get }
}

enum A: String, CaseIterable, CaseNamed {
case a1, a2, a3
}

enum B: String, CaseIterable, CaseNamed {
case b1, b2, b3
}

extension CaseNamed where Self: CaseIterable {
static var caseNames: [String] {
self.allCases.map { "\($0)" }
}
}

var enums: [CaseNamed.Type] = [A.self, B.self]

enums.flatMap { $0.caseNames }

Now of course you might also want this CaseNamed protocol to do other things, so you can add those other things. But you need to think in terms of the calling code and what it fundamentally needs to do its job.

Can I make a Swift enum generic, so I can use its cases to infer a type for a generic class?

You've got it backwards, you shouldn't infer the generic type depending on the enum value, because that means you want to determine some compile-time thing (generic type) with a value that is possibly known at runtime (the value of the enum).

Therefore, we need to make the type parameter a compile-time-only thing, i.e. also a type parameter.

You first introduce a VehicleTypeProtocol, and a struct implementing that protocol for each of the enum case:

protocol VehicleTypeProtocol {
// this associated type creates the link between a vehicle type and a config type
associatedtype ConfigType: Config
// this is so that we can assign to Vehicle.type
static var type: VehicleType { get }
}

struct Car : VehicleTypeProtocol {
typealias ConfigType = CarConfig
static var type: VehicleType { .car }
}

struct Bike : VehicleTypeProtocol {
typealias ConfigType = BikeConfig
static var type: VehicleType { .bike }
}

struct Scooter: VehicleTypeProtocol {
typealias ConfigType = BikeConfig
static var type: VehicleType { .scooter }
}

And then the initialiser can be implemented like this:

init<T: VehicleTypeProtocol>(type: T.Type, config: C) where T.ConfigType == C {
self.type = T.type
self.config = config
}

Usage:

let bike = Vehicle(type: Bike.self, config: .mtb)

But man, this is convoluted...

Swift Enum associated values conforming to single protocol

I've found the solution in case anyone will need this too.

enum ContentType {
case content1(Type1 & Refreshable)
case content2(Type2 & Refreshable)
case content3(someLabel: Type3 & Refreshable)

func refreshMe() {
let caseReflection = Mirror(reflecting: self).children.first!.value
(caseReflection as? Refreshable)?.refresh() //If associated type doesn't have label

let refreshable = Mirror(reflecting: caseReflection).children.first?.value as? Refreshable
refreshable?.refresh() //If associated type has label
}
}

Swift protocol with associatedtype (ambiguous for type lookup)

Generally when covering generics in the context of protocols, the generic typeholder is seen as representable by an associatedtype of the protocol. In your example this would be SectionIdentifierEnum, which acts as a placeholder for a constrained type. SectionIdenfierEnum is not, however, a protocol by itself, so cannot use it as a type constraint in a generic method. You can, however, use it as the type itself in your test(...) method.


Swift 3.1

Now, currently (Swift 3.1), you can't add sophisticated type constrains to an associatedtype. You could can, however, supply a default implementation available only for the case the where Self derives from UIViewController and implements the RequiresEnum protocol by setting the SectionIdentifierEnum type to the concrete RequiresEnumDefault type. The latter will ascertain that the associated RawValue is String for this default implementation, as the RawValue of the concrete RequiresEnumDefault type is String.

E.g.:

// Swift 3.1
// ---------
// Types that implement this protocol mustn't necessarily use a
// `SectionIdentifierEnum` type where `SectionIdentifierEnum.RawValue`
// is constrained to equal `String`.
protocol RequiresEnum: class {
associatedtype SectionIdentifierEnum: RawRepresentable

func test(identifier: SectionIdentifierEnum)
}

enum RequiresEnumDefault: String {
case `default`
}

// This extension, however, is only available for types that use
// `RequiresEnumDefault ` as the concrete type of `SectionIdentifierEnum`
// (in which case `SectionIdentifierEnum.RawValue` is `String`).
extension RequiresEnum where Self: UIViewController, SectionIdentifierEnum == RequiresEnumDefault {

func test(identifier: SectionIdentifierEnum) {
print(identifier.rawValue)
}
}

// Example usage.
class MyViewController : UIViewController, RequiresEnum {
typealias SectionIdentifierEnum = RequiresEnumDefault
// ...
}

let foo = MyViewController()
foo.test(identifier: RequiresEnumDefault.default)
// prints "default" (using extension:s default implementation)

Above, the default implementation of test(...) is only available when SectionIdentifierEnum equals the concrete type RequireEnumDefault (and Self derives from UIViewController ...). If instead you want it to only be available when SectionIdentifierEnum is any enum with String typed RawValue, you modify the type constraint of the extensions accordingly:

protocol RequiresEnum: class {
associatedtype SectionIdentifierEnum: RawRepresentable

func test(identifier: SectionIdentifierEnum)
}

enum RequiresEnumDefault: String {
case `default`
}

extension RequiresEnum where Self: UIViewController, SectionIdentifierEnum.RawValue == String {

func test(identifier: SectionIdentifierEnum) {
print(identifier.rawValue)
}
}

// Example usage.
enum EnumWithStringRawValue: String {
case foo
}

class MyViewController : UIViewController, RequiresEnum {
typealias SectionIdentifierEnum = EnumWithStringRawValue
// ...
}

let foo = MyViewController()
foo.test(identifier: EnumWithStringRawValue.foo)
// prints "foo" (using extension:s default implementation)

Once Swift 4 is released, you'll be able to add more sophisticated constraints to associatedtype:s, as per the implementation of Swift evolution proposal:

  • SE-0142: Permit where clauses to constrain associated types

In which case the above can be modified into:

// Swift 4
// -------
// Here, all types that implement this protocol must use a
// `SectionIdentifierEnum` type where `SectionIdentifierEnum.RawValue`
// is equal to `String`.
protocol RequiresEnum: class {
associatedtype SectionIdentifierEnum: RawRepresentable
where SectionIdentifierEnum.RawValue == String

func test(identifier: SectionIdentifierEnum)
}

enum RequiresEnumDefault: String {
case `default`
}

// For the specific case where `SectionIdentifierEnum` equals
// `RequiresEnumDefault` (and where `Self` derives from `UIViewController`),
// this default implementation is readily available.
extension RequiresEnum where Self: UIViewController, SectionIdentifierEnum == RequiresEnumDefault {

func test(identifier: SectionIdentifierEnum) {
print(identifier.rawValue)
}

}

// Example usage.
class MyViewController : UIViewController, RequiresEnum {
typealias SectionIdentifierEnum = RequiresEnumDefault
// ...
}

let foo = MyViewController()
foo.test(identifier: RequiresEnumDefault.default)
// prints "default" (using extension:s default implementation)

Likewise modifying the constraint on the default implementation of test(...) not only to the case where SectionIdentifierEnum equals RequiresEnumDefault (but to any enum: in this case we know such enum will always have a RawValue String, due to the constraint on the associatedtype in the protocol definition).

protocol RequiresEnum: class {
associatedtype SectionIdentifierEnum: RawRepresentable
where SectionIdentifierEnum.RawValue == String

func test(identifier: SectionIdentifierEnum)
}

enum RequiresEnumDefault: String {
case `default`
}

// From the constraint on the `RawValue` of `SectionIdentifierEnum`
// above, we know that `RawValue` equals `String`.
extension RequiresEnum where Self: UIViewController {

func test(identifier: SectionIdentifierEnum) {
print(identifier.rawValue)
}
}

// Example usage.
enum EnumWithStringRawValue: String {
case foo
}
class MyViewController : UIViewController, RequiresEnum {
typealias SectionIdentifierEnum = EnumWithStringRawValue
// ...
}

let foo = MyViewController()
foo.test(identifier: EnumWithStringRawValue.foo)
// prints "foo" (using extension:s default implementation)


Related Topics



Leave a reply



Submit