Ferran Maylinch
Ferran Maylinch

Reputation: 11539

How to constraint generic type to another generic type in Swift?

I'd like to do something like this:

class Config<T> {
  func configure(x:T)
  // constraint B to be subclass of A
  class func apply<A,B:A>(c:Config<A>, to:B) {
    c.configure(to)
  } 
}

So later, for example, I can apply a Config to a UILabel:

class RedViewConfig<T:UIView> : Config<T> {
  func configure(x:T) {
    x.backgroundColor = .redColor();
  } 
}

let label = UILabel() 
Config.apply(RedViewConfig(), to:label)

Or extend Config classes:

class RedLabelConfig<T:UILabel> : RedViewConfig<T> {
  func configure(x:T) {
    super.configure(x)
    x.textColor = .redColor();
  } 
}

Config.apply(RedLabelConfig(), to:label)

I tried to do it, but I couldn't constraint classes. So I tried with protocols and associated types, but when subclassing I found problems (like this) when overriding the associated type.

Upvotes: 1

Views: 171

Answers (2)

Rob Napier
Rob Napier

Reputation: 299663

Classes make this way too complicated. Inheritance is almost always a bad idea in Swift if you can possibly avoid it.

Structs, though closer, still make this a bit over-complicated and restrictive.

Really, these configurators are just functions. They take a thing and they do something to it, returning nothing. They're just T -> Void. Let's build a few of those.

func RedViewConfig(view: UIView) { view.backgroundColor = .redColor() }
func VisibleConfig(view: UIView) { view.hidden = false }

And we can use them pretty easily:

let label = UILabel()
VisibleConfig(label)

We can compose them (like super, but without the baggage) if their types are compatible:

func RedLabelConfig(label: UILabel) {
    RedViewConfig(label)
    label.textColor = .redColor()
}

We can pass them around in data structures, and the compiler will apply the right covariance for us:

let configs = [RedLabelConfig, VisibleConfig]
// [UILabel -> ()]
// This has correctly typed visibleConfig as taking `UILabel`,
// even though visibleConfig takes `UIView`

// And we can apply them
for config in configs { config(label) }

Now if we want other syntaxes, we can build those pretty easily too. Something more like your original:

func applyConfig<T>(f: T -> Void, to: T) {
    f(to)
}
applyConfig(VisibleConfig, to: label)

or even closer to your original:

struct Config {
    static func apply<T>(config: T -> Void, to: T) { config(to) }
}

Config.apply(VisibleConfig, to: label)

The point is that just using functions here makes everything very flexible without adding any of the complexity of class inheritance or even structs.

Upvotes: 1

GoZoner
GoZoner

Reputation: 70255

Do you actually need the generic parameter B? If your argument to: was typed as A as well, it could be any subtype of A. Like such:

class View {}
class LabelView : View {}

class Config<T> {
  func configure(x:T) { print ("Configured: \(x)") }  
}

func applyConfig<A> (c:Config<A>, to:A) {
  c.configure(to)
}

applyConfig(Config<View>(), to: LabelView())

Upvotes: 1

Related Questions