Currently revisiting core modules of the project to review applicability of existentials there and possibly replace them with generics/opaque types, and while to some extent that is an easy job, some of cases start reminding me why I went with existentials instead of generics in the first place, and make me question where to draw the line.
Of course, it differs from project to project, but I think there is possible to make rough distinction, as we have libraries/frameworks, where it is probably better to sacrifice some internal convenience and use generics, or have cases where there is literally any type possible. There are performance-critical code, where you want to avoid abstractions at many levels, and clearly want to avoid existentials, yet it usually more isolated or domain-specific. All this cases brings almost no confusion for me, as it has certain specific criteria to base decision on. The use cases I most concerned about are for more or less "regular" client apps, with no demands on being extra-performant, just work reasonably, so difference of using existential vs generics might be not visible at all.
As an example, consider following part of UI code:
open class DataViewController<Provider, Listener>: UIViewController
where Provider: DataCollectionProvider, Listener: ListActionsListener {
private let provider: Provider
private let listener: Listener
public init(provider: Provider, listener: Listener) {
self.provider = provider
self.listener = listener
super.init(nibName: nil, bundle: nil)
}
}
public struct DataView<Provider, Listener>: UIViewControllerRepresentable
where Provider: DataCollectionProvider, Listener: ListActionsListener {
private let provider: Provider
private let listener: Listener
public init(provider: Provider, listener: Listener) {
self.provider = provider
self.listener = listener
}
public typealias UIViewControllerType = DataViewController<Publisher, Listener>
public func makeUIViewController(context: Context) -> UIViewControllerType {
UIViewControllerType(
viewModelPublisher: viewModelPublisher,
provider: provider,
commands: commands
)
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Not counting that generic is a bit of verbose, it also goes viral if you want to have a wrapper or pass dependencies down as well. Comparing to using existentials, which look much cleaner:
open class DataViewController: UIViewController {
private let provider: any DataCollectionProvider
private let listener: any ListActionsListener
public init(provider: any DataCollectionProvider, listener: any ListActionsListener) {
self.provider = provider
self.listener = listener
super.init(nibName: nil, bundle: nil)
}
}
public struct DataView: UIViewControllerRepresentable {
private let provider: any DataCollectionProvider
private let listener: any ListActionsListener
public init(provider: any DataCollectionProvider, listener: any ListActionsListener) {
self.provider = provider
self.listener = listener
super.init(nibName: nil, bundle: nil)
}
public typealias UIViewControllerType = DataViewController
public func makeUIViewController(context: Context) -> UIViewControllerType {
UIViewControllerType(
viewModelPublisher: viewModelPublisher,
provider: provider,
commands: commands
)
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
That seems more appealing to me as there is less code to write and deal with, and from the performance perspective seems to have no difference. As this example of more generic code, which actually can have any provider & listener passed into, the issue actually arises every time you need to have stored properties.
Given all of that, it seems that one can go with existentials in this case and don't bother, which might be true, yet what I have observed over the journey of migration to reduced existentials, it seems that using more generics and less existentials actually leads to better design as current tools allow to avoid verbose parts on usage side.
So, given that aim is for better design choices and code clarity, rather than performance details, what are the thoughts on such "day-to-day" use of generics and existentials?