So an idle holiday musing.
Take a peek at the following function taken from an image caching service.
private func cachedImage(for path: String?) -> AnyPublisher<UIImage?, Never> {
guard let path = path else {
return Just(nil)
.eraseToAnyPublisher()
}
if let image = imageCache.object(forKey: NSString(string: path)) {
return Just(image)
.eraseToAnyPublisher()
}
return image(for: path)
.handleEvents(receiveOutput: { [weak imageCache] (image) in
imageCache?.setObject(image, forKey: NSString(string: path))
})
.eraseToAnyPublisher()
}
Notice anything, uh... shall we say... redundant?
Yep. It's the eraseToAnyPublisher
operator, needed to convert a specific type to our type-erased AnyPublisher return type.
That convention is probably the one single thing that makes doing anything with Combine feel cumbersome and clunky as opposed to doing the same thing in RxSwift. But what if we could do something like the following?
extension Just: TypeConvertable {
var convertable: AnyPublisher<Output, Never> {
self.eraseToAnyPublisher()
}
And, that, along with a few other definitions of the same kind, would let us do...
private func cachedImage(for path: String?) -> AnyPublisher<UIImage?, Never> {
guard let path = path else {
return Just(nil)
}
if let image = imageCache.object(forKey: NSString(string: path)) {
return Just(image)
}
return image(for: path)
.handleEvents(receiveOutput: { [weak imageCache] (image) in
imageCache?.setObject(image, forKey: NSString(string: path))
})
}
Much cleaner. This could be accomplished by something similar to the "ExpressibleBy" protocol...
protocol TypeConvertable {
associatedtype ReturnType
var convertable: ReturnType { get }
}
So we're not really talking about "automatic" type conversion in as much as we're discussing a mechanism for defining specific types of predefined conversions. Define a type conversion, and the compiler could "fix" the difference between a provided type and an expected type. In the above case we're talking about the type required by the function's return type.
Of course, defined this way, we could have only one conformance on a given type. So perhaps we could simply tell the compiler that this type provides conversions and have it then match on the function return type.
protocol TypeConvertable {}
enum SomeEnum: Int {
case a
case b
}
extension SomeEnum: TypeConvertable {
func convertable() -> Int { self.rawValue }
func convertable() -> Double { Double(self.rawValue) }
}
This would let us pass our enum to any function that desired an Int or Double as a parameter. That kind of thing might be frowned upon by some, so consider...
struct MyView: ViewBuilder {
var body: View {
TextView("Hi there!")
}
}
extension ViewBuilder: TypeConvertable {
var convertable: UIView {
self.body.asUIView()
}
}
Adding an UIView
TypeConvertable
conformance to ViewBuilder
allows any view builder definition to be passed as a parameter to any function expecting a UIView.
class MyViewController: UIViewController {
override func viewDidLoad() {
view.addSubview(MyView())
}
}
And without requiring us to do...
view.addSubview(MyView().asUIView())
Again, this isn't automatic conversion. We're explicitly defining conversions that can be accomplished safely and that we've considered beforehand.
So that's the idea. From my perspective, continually erasing types or explicitly defining rawValues or view conversions is boilerplate that gets in the way of expressing our actual intent and, worse, continually exposes the underpinnings of the abstractions we're trying to express.
Thoughts?