0

I've created a 'configure' infix operator '=>' which lets me configure objects inline, allowing me to both define and initialize properties and/or call methods/functions at the assignment level.

For instance, instead of this, which requires an initializer...

let redSquare = SquareView()

init(){ // Some init somewhere

    redSquare.backgroundColor = .red

}

I can simply do this...

let redSquare = SquareView() => { $0.backgroundColor = .red }

The 'configure' operator is implemented like this...

infix operator =>

public protocol Configurable {}

extension Configurable {

    public typealias ConfigureDelegate = (Self) -> Void

    @discardableResult
    public static func => (this:Self, _ delegate:ConfigureDelegate) -> Self {
        delegate(this)
        return this
    }
}

extension NSObject : Configurable {}

Note: You could also implement this as a member function like below, but I prefer the infix operator as it doesn't clutter the . (dot) completion.

public protocol Configurable {}

extension Configurable {

    public typealias ConfigureDelegate = (Self) -> Void

    @discardableResult
    public func configure(delegate:ConfigureDelegate) -> Self {
        delegate(self)
        return self
    }
}

extension NSObject : Configurable {}

As you can see, I already make NSObject conform to this protocol, but realistically, this should be able to be utilized by anything that needs to set properties not passed through an initializer, or anything that requires a method/function to be called as part of its initialization.

Currently, this means I just have to keep adding this everywhere (for non-NSObject-based items)...

extension MyStruct     : Configurable {}
extension MyClass      : Configurable {}
extension MyOtherClass : Configurable {}

This one's not allowed (which really annoys me!)

extension AnyObject : Configurable {}

In C# for instance, you can extend everything by simply extending 'object' since that is the base for everything (ironically including value types), but it doesn't look like there's anything similar in Swift.

Is there, or do I just have to keep manually adding those conformances?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286

2 Answers2

1

If all you want is an operator that applies a closure to whatever type you throw at it, than a protocol isn't really the right approach here. A plain generic operator (function) will do the trick:

infix operator =>

@discardableResult func => <T>(this: T, _ delegate: (T) -> ()) -> T {
    delegate(this)
    return this
}

This can be used in exactly the way you outline, without needing to extend anything.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
Marcel Tesch
  • 184
  • 3
  • 15
  • I didn't know you could define an operator at the global level. I thought you had to do it as a static of a type. This is perfect! I love when something so obvious is missed! Thanks! – Mark A. Donohoe Oct 17 '18 at 14:17
  • Had to make one small change because with this approach, theoretically you could pass in optionals (explicit or implicitly-unwrapped) which causes issues with the return. Using this approach addresses that. – Mark A. Donohoe Oct 17 '18 at 14:53
  • Good thinking, I didn't consider optionals. The operator could also be marked as rethrowing, in case one wants to allow configurations that may throw errors, but that's maybe not so relevant in this usecase. – Marcel Tesch Oct 17 '18 at 15:12
  • Why is `this` an IUO argument? Those are deprecated, and passing `nil` in will crash because a non-optional `T` is returned. – Martin R Oct 17 '18 at 15:24
  • Are you sure about that? Since T comes from the argument itself, the only way `this` could be `nil` is if T itself was an optional (or IUO), meaning `this` would itself be optional so it wouldn't crash. (I.e. it can only be nil if it's an IUO of an optional. Make sense?) – Mark A. Donohoe Oct 17 '18 at 15:29
  • @MarqueIV: `let a: Int? = nil ; let b = a => { _ in } ; print(b)` crashes with “Fatal error: Unexpectedly found nil while unwrapping an Optional value ...” – Martin R Oct 17 '18 at 15:35
  • Aaah! I see what you mean. Just ran through a bunch of scenarios. Reverting back to the prior version. – Mark A. Donohoe Oct 17 '18 at 15:40
  • @MartinR, now in Xcode 10.x (but not in Xcode 9.x) if I use it on any IBOutlets defined as IUOs, all the configure methods force me to deal with explicit optionality. How can this be updated to only call the configure delegate if there's an instance without knowing if T is an optional? AFAIK, you can't test if T is an optional, nor can you check for nil since T isn't an optional type to the generic. How do you get around that limitation? – Mark A. Donohoe Oct 17 '18 at 15:48
  • That was one advantage of the protocol version. You were guaranteed an instance. – Mark A. Donohoe Oct 17 '18 at 15:48
  • Wouldn't a separate overload to handle the optional case suffice? https://gist.github.com/tesch/e4819badd0036c2cf72bb3774c837e2f – Marcel Tesch Oct 17 '18 at 16:08
  • @Marcel, I tried the overload, but now I'm getting ambiguous operator errors. Have you tried passing in non-optionals, normal optionals, and IUOs to the code you posted? Maybe I'm doing something wrong. – Mark A. Donohoe Oct 17 '18 at 21:39
  • Add this line to your gist and you'll see the error... `bar => { unwrappedBar in print("Configured again") }` – Mark A. Donohoe Oct 17 '18 at 21:44
  • In that case you would need to be explicit about whether you want the closure argument to be optional or not. `bar => { (unwrappedBar: OperationQueue) in }` This still saves you from having to manually unwrap the value though. Not perfect, but maybe useful in some cases. – Marcel Tesch Oct 18 '18 at 14:20
0

I've created a 'configure' infix operator ...For instance, instead of this, which requires an initializer...

let redSquare = SquareView()
init(){ // Some init somewhere
    redSquare.backgroundColor = .red
}

I can simply do this...

let redSquare = SquareView() => { $0.backgroundColor = .red }

No need for a new operator. This functionality already exists in Swift; it's called a closure (anonymous function):

 let redSquare : SquareView = {
      let sv = SquareView()
      sv.backgroundColor = .red
      return sv
 }()

In the case of an NSObject derivative, where the existence of an init() is guaranteed to the compiler, you can compress the notation with my lend utility:

func lend<T> (_ closure: (T)->()) -> T where T:NSObject {
    let orig = T()
    closure(orig)
    return orig
}

Thus:

let redSquare = lend { (sv:SquareView) in sv.backgroundColor = .red }
Community
  • 1
  • 1
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Hi Matt! You've answered a bunch of my questions before. Thanks! :) One thing I prefer about the infix-with-closure over the closure-only approach is the latter only works at initialization time. The former can work at *any* time, including when you already have an instance. For instance (ha!), if I already have a RedSquare, I can still do myRedSquare => {.$0.bla(); $0.blabla() } which can save a lot of typing, let alone getting free scoping when setting a bunch of properties or calling methods, and you can pass that inline if needed since again, it's a statement. – Mark A. Donohoe Oct 17 '18 at 14:22
  • Sure, good point. I was going by the particular use case you cited. Do you want me to leave this here? I can just delete it if it's not relevant. – matt Oct 17 '18 at 14:25
  • No, it's relevant! Some people may only want initialization-time. Plus, your 'lend' function may help others. I think it adds to the topic! – Mark A. Donohoe Oct 17 '18 at 14:26