UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Generic closures sum - invariance, covariance, contravariance

Forums > Swift

Could you please help me with a hint for one question? I don't understand why one code does work ok, and another similar 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, where I can pass UILabel. And func + can accept two closures, containing different types - UIView and UILabel.

But 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 more or less understand why second code gives an error. But I can't understand why first code gives no errors. Do you have any idea? If you know some guides, docs or articles where I can read about it, I'll really appreciate if you send some links to me. Thanks!

2      

There is some very strange business going on. It could perhaps be a bug. Consider the following modifications:

First Test

Change

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

to

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

The compiler will then not allow the function + to be called because the two closures passed in as parameters in the declaration of s3 are not the same, as is expected. There is certainty here.

Second Test

Attempt the following: print the types of the closures passed in as parameters from the closure returned by + as follows:

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

        print(type(of: rhs))
        print(type(of: lhs))
    }
}

Notice that s1 and s2 have type (UILabel) -> (), which is somwhat strange since s1 has type (UIView) -> (). It seems that the compiler allows for the coersion of s1 to (UILabel) -> () in order to satisfy the constraints of the generic function. In fact,

let s_weird = s1 as! StyleClosure<UILabel>

generates a warning that the cast always succeeds (which is indeed true) and that coercion should be used instead.

Now here comes the possible bug:

let thinksIsTrue = s1 is StyleClosure<UILabel>
print(thinksIsTrue)

The compiler issues a warning that the is test is always true. However, the test actually evaluates to false.

I have not run into a situation quite like this so there may be something deeper that I am missing with implicit coercion. There is some reason why the compiler might allow this. Since the UIView closure could not modify a UILabel in any strange way since UILabel is a UIView, coercion behind the scenes to a more specific closure type would allow for the possibility of calling both closures while satisfying the generic constraint, allowing us to call the closure with a narrower range of types. Since the intention is not very clear, this is perhaps undesirable.

If anybody has any other explanations let us know!

3      

Actually I've got an answer at Stackoverflow. It's due to kind of sophisticated computer science layer Swift language features - invariance, covariance and contravariance :). Link to the answer: https://stackoverflow.com/a/62342173/5186503

Copying the answer here: 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())

2      

@n0an Could you explain the result of the is test? This concept is new to me I'll take a look at it. Thanks for your answer, interesting stuff!

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.