Combining enums with same choices

Hi,
How can I write the following without repeating the choices?

import Foundation

public protocol MyConnectionState {
    var value: String { get }
    func isNominal() -> Bool
}

public enum MyDownloadState: String, MyConnectionState {
    
    public var value: String { return self.rawValue }

    public func isNominal() -> Bool {
        return self == .connected
    }
    
    case idle = "idle"
    case connecting = "connecting"
    case connected = "connected"
    case failing = "failing"
    case terminated = "terminated"
}

public enum MyUploadState: String, MyConnectionState {
    
    public var value: String { return self.rawValue }
    
    public func isNominal() -> Bool {
        return self == .idle || self == .connected
    }
    
    case idle = "idle"
    case connecting = "connecting"
    case connected = "connected"
    case failing = "failing"
    case terminated = "terminated"
}

Nope.

To be a bit more helpful, while there's no way to share cases between enums, this seems more like something where you want just a single enum, not a protocol with multiple conforming enums. If the name of the enum is really that important you can use a typealias to create different names, but that doesn't seem to be the case here. If you need unique state for upload and download, you could also make enum generic to some payload type in one of the cases, like a completion value, rather than using raw `Strings (which don't seem to serve a purpose here anyway).

My use case is that download and upload both could share the enum values but the protocol is there because each needs to have their own implementation of isNominal()

I also need the string representation (ok, could be inferred instead of explicitly stated) because those are used in an API that needs strings.

So it boils down to a pattern that I encounter frequently: a common enum with different extra functionality for “sub enums”

While I could simply make a struct that contains the common enum and follows the given protocol, I do face problems with that approach as soon as I want to make certain parts of my code that handle that state not repeat and deal with both versions. It also makes it less elegant to use the enum state from a user perspective.

An option could be to use a single enum with a generic parameter to represent the "data transfer type", and use a kind of visitor pattern to implement isNominal:

import Foundation

public protocol MyConnectionState {
  var value: String { get }
  func isNominal() -> Bool
}

public enum MyDataTranferState<DataTransferType: MyDataTransferType>: String {
  case idle = "idle"
  case connecting = "connecting"
  case connected = "connected"
  case failing = "failing"
  case terminated = "terminated"
}

extension MyDataTranferState: MyConnectionState {
  public var value: String { rawValue }

  public func isNominal() -> Bool {
    DataTransferType.isNominal(for: self)
  }
}

public protocol MyDataTransferType {
  static func isNominal(for: MyDataTranferState<Self>) -> Bool
}

public enum Download: MyDataTransferType {
  public static func isNominal(for dataTransferState: MyDataTranferState<Self>) -> Bool {
    dataTransferState == .connected
  }
}

public enum Upload: MyDataTransferType {
  public static func isNominal(for dataTransferState: MyDataTranferState<Self>) -> Bool {
    dataTransferState == .idle || dataTransferState == .connected
  }
}

public typealias MyDownloadState = MyDataTranferState<Download>
public typealias MyUploadState = MyDataTranferState<Upload>

assert(!MyDownloadState.idle.isNominal())
assert(MyDownloadState.connected.isNominal())
assert(!MyDownloadState.terminated.isNominal())

assert(MyUploadState.idle.isNominal())
assert(MyUploadState.connected.isNominal())
assert(!MyUploadState.terminated.isNominal())

With this approach you would be splitting only the part that actually changes (that is, isNominal) without also duplicating the structure of the type.

1 Like

What kind of API? Some sort of backend API? In any case, you have a lot of other options for those values, so there's no need to tie your cases to their values at all. You can use a computed property to provide the value.

A simpler solution may be just to back up a level and separate your connection state from whatever the isNominal value actually represents. For instance, using a struct to wrap a few enum values.

struct Transfer {
  enum Kind { case upload, download }
  enum State { case idle, connecting, connected, failing, terminated }

  let kind: Kind
  var state: State

  var isNominal: Bool {
    switch kind {
    case .upload: ...
    case .download: ...
    }
  }

  var value: String {
    switch state {
    // return proper string
  }
}

Abstracting with a protocol doesn't seem to get you anything here.

1 Like

@Jon Kind of awkward if I have the two enums being part of two publishers that give you the current state

My use case: a sync framework that allows the app to observe connection state changes (up- download have their own connection). The textual representation is for debug logging and the enum for type safe app logic. Framework only knows about what is nominal and there is a global method returning overall nominal state and two publishers for each down and upload

Kind of awkward if I have the two enums being part of two publishers that give you the current state

How so? This is pretty typical Swift design.

The textual representation is for debug logging and the enum for type safe app logic.

Swift already has solutions for debug strings: CustomStringConvertibble and CustomDebugStringConvertible.

Yes I am still in the design phase and string representation is my least concern right now.

Combining both states in a struct that then is the value of a publisher makes no sense if there are two connections with independent state. With awkward I mean that you get combined events for aggregated state and I prefer a single publisher per download or upload that only contains state for the given *load

I still don’t understand your concern. If you want two publishers, make two publishers.

1 Like
Terms of Service

Privacy Policy

Cookie Policy