Anton Novoselov
Anton Novoselov

Reputation: 769

Swift generic closures sum

Could you please tell, why one code does work ok and another doesn't?

This code works ok:

typealias StyleClosure<T: UIView> = (T) -> ()

func +<T>(lhs: @escaping StyleClosure<T>,
          rhs: @escaping StyleClosure<T>) -> StyleClosure<T> {
    return { (value: T) -> Void in
        lhs(value)
        rhs(value)
    }
}

let s1: StyleClosure<UIView> = { (view: UIView) in
    view.layer.cornerRadius = 1
}

let s2: StyleClosure<UILabel> = { (view: UILabel) in
    view.font = UIFont.systemFont(ofSize: 14)
}

let s3 = s1 + s2

s3 is closure, I can pass UILabel to it. And func + can accept two closures, containing different types - UIView and UILabel.

But the following code gives an error:

class Styler<T: UIView> {
    private let closure: (T) -> ()
    init(_ closure: @escaping (T) -> ()) {
        self.closure = closure
    }
    func apply(_ view: T) {
        self.closure(view)
    }
}
func +<T>(lhs: Styler<T>, rhs: Styler<T>) -> Styler<T> {
    return Styler { (value: T) in
        lhs.apply(value)
        rhs.apply(value)
    }
}

let styler1: Styler<UILabel> = Styler { (label: UILabel) -> Void in
    label.backgroundColor = UIColor.red
}

let styler2: Styler<UIView> = Styler { (view: UIView) -> Void in
    view.backgroundColor = UIColor.green
}

let styler3 = styler1 + styler2

This code gives following compile error:

Cannot convert value of type 'Styler<UIView>' to expected argument type 'Styler<UILabel>'

I kind of understand, why the second code gives an error. Do you have any idea, why first code gives no errors?

Upvotes: 2

Views: 249

Answers (1)

David Pasztor
David Pasztor

Reputation: 54745

You are running into the issue of Swift generic coercion misunderstanding. Generic types in Swift are invariant, which means that Styler<A> and Styler<B> are completely unrelated types even if A and B are related (subclasses for instance).

This is why Style<UILabel> and Styler<UIView> are unrelated. However, closures (and hence functions) are variant (as explained here) - covariant on the return type, and contravariant on the parameter types, this is why your first example works.

Because of this, you can pass a UILabel to a Styler<UIView>.apply, since that is a simple function call, which accepts subclasses of the declared input argument type.

let styler1: Styler<UIView> = Styler { (label: UIView) -> Void in
    label.backgroundColor = UIColor.red
}

let styler2: Styler<UIView> = Styler { (view: UIView) -> Void in
    view.backgroundColor = UIColor.green
}

let styler3 = styler1 + styler2

styler1.apply(UILabel())

Upvotes: 5

Related Questions