[Pitch] Enum Case Constraints

Background

I'm working on a server controlled user feedback flow right now, and I've found that it's represented quite nicely as a set of enums:

enum FeedbackScreen {
	indirect case questions(header: String?, subHeader: String?, questions: [String], next: FeedbackScreen)
	case confirmation(header: String, subHeader: String?)
}

In practice, however, I'm still left with some run-time type checking:

class ConfirmationViewController: UIViewController {
	let headerLabel = UILabel()
	let subHeaderLabel = UILabel()
	
	required init(with confirmationScreenModel: FeedbackScreenModel) {
		super.init(nibName: nil, bundle: nil)
		
		// Can't enforce this at compile time
		guard case let .confirmation(header: header, subHeader: subHeader) = confirmationScreenModel else {
			fatalError("I can only take confirmation models!")
		}
		
		headerLabel.text = header
		subHeaderLabel.text = subHeader
	}
}

I could alternatively choose to wrap the usable properties for each enum in a struct, but that just adds verbosity and creates an additional layer of indirection that makes the enum approach less effective overall:

enum FeedbackScreenModel {
	struct QuestionsModel {
		var header: String?
		var subHeader: String?
		var questions: [String]
		var nextScreenModel: FeedbackScreenModel
	}
	
	struct ConfirmationModel {
		var header: String
		var subHeader: String?
	}
	
	indirect case questions(QuestionsModel)
	case confirmation(ConfirmationModel)
}

Proposed Solution

The goal of this pitch is to improve the compile-time safety for this enum use-case, as well as make enums themselves more practical to use as-is without the need for additional layers of indirection.

// Typealiased
typealias ConfirmationModel = FeedbackScreenModel where case .confirmation

// With a variable
let confirmationScreenModel: FeedbackScreenModel where case .confirmation = .confirmation(header: "Hello", subHeader: "World")

// With a function
func printConfirmation(confirmationModel: FeedbackScreenModel where case .confirmation) {...}

Following the Enum type with a where case clause would, at compile time, generate a hidden protocol that only the specific enum case conforms to. It can then be used as follows:

func printConfirmation(confirmationModel: FeedbackScreenModel where case .confirmation) {
	print("\(confirmationModel.0) \(confirmationModel.1)")
}

// Invalid, the enum case constraint must be explicit
let bar1 = FeedbackScreenModel.confirmation(header: "bar", subHeader: "1")
printConfirmation(confirmationModel: bar1)

// Valid, prints 'bar Optional("2")'
let bar2: FeedbackScreenModel where case .confirmation = .confirmation(header: "bar", subHeader: "2")
printConfirmation(confirmationModel: bar2)

// Valid, prints 'bar Optional("3")'
let bar3: FeedbackScreenModel = .confirmation(header: "bar", subHeader: "3")
if let confirmationModel where case .confirmation = bar3 as? FeedbackScreenModel {
	printConfirmation(confirmationModel: confirmationModel)
}

// Valid, prints 'bar Optional("4")'
let bar4: FeedbackScreenModel = .confirmation(header: "bar", subHeader: "4")
switch bar4 {
case let confirmationModel as FeedbackScreenModel where case .confirmation:
	printConfirmation(confirmationModel: confirmationModel)
default:
	break
}

Note that the above printConfirmation() method used tuple notation for accessing properties. This is to match how Swift enums currently store associated values as tuples. On implementation of SE-0155 (Normalize Enum Case Representation), much of what is proposed above evolves into:

// Typealiased (post SE-0155)
typealias ConfirmationModel = FeedbackScreenModel where case .confirmation(header:subHeader:)

// With a variable (post SE-0155)
let confirmationScreenModel: FeedbackScreenModel where case .confirmation(header:subHeader:) = .confirmation(header: "Hello", subHeader: "World")

// printConfirmation (post SE-0155)
func printConfirmation(confirmationModel: FeedbackScreenModel where case .confirmation(header:subHeader:)) {
	print("\(confirmationModel.header) \(confirmationModel.subHeader)")
}

Thoughts?

I don't think this can be the implementation, as enum cases are all of the same type and either the type conforms or not.

That is, in fact, the encouraged solution. In evaluating SE-0155, the core team wrote:

...very few cases carry a large number of associated values. As the amount of information which the case should carry grows, it becomes more and more interesting to encapsulate that information in its own struct...

2 Likes