#if can't handle the number of arguments correctly

I am trying to write something like:

struct Handlers {
#if os(macOS)
    var contentChanged: (_ isEdited: Bool) -> Void
#elseif os(iOS)
    var contentChanged: (String, String, Bool) -> Void
#endif
}

Handlers(
    contentChanged: {
#if os(macOS)
        contentChanged($0)
#elseif os(iOS)
        contentChanged($0, $1, $2)
#endif
    },
)

When I compile iOS it passes, but when I compile macOS it gives me these error:

Cannot convert value of type '(Bool, _, _) -> Void' to expected argument type '(Bool) -> Void'
Cannot infer type of closure parameter '$1' without a type annotation

Any solution?

#if seems to be doing just fine!

Your closure has to have some type, which doesn't change between macOS and iOS. That type must have (at least) three parameters since you have to be able to call contentChanged(_:_:_:) on iOS.

As the error message tells you, on macOS, there's nothing to determine what the type of the second and third parameters must be. Therefore, you'll need to give an explicit type for the closure exactly where indicated. In other words, you'll have to fill in the blanks: (_, _, _) -> Void in ...

There seem to be several things at play here.
First off, when you write that opening { in the initializer call right after the label, that's where the type of the closure is determined. You're basically using a short form syntax, but the fully written out closure would be:

Handlers(
    contentChanged: { (<#parameters#>) -> <#return type#> in

This is then not inside any compiler directive at all. When you compile on macOS, the property contentChanged has the type (Bool) -> Void, so the initializer would be synthesized as init(contentChanged: (Bool) -> Void).
I cannot see what exactly you do in your code from the screenshot, but apparently you pass a closure that has the type (String, String, Bool) -> Void.

Note that in the example call you provide in the second part of the code snippet, your code would still imply that, as like I said above, you begin the closure with the {! Then inside that closure with type (Bool) -> Void, you use a compiler directive to call a closure variable you must have defined before.
What type that closure has is not defined in code, but here you try to call it with that bool $0 as single parameter if you run on macOS and you try to call it with all three parameters if on iOS.

This means using the compiler directives inside the ad-hoc closure you pass as parameter shouldn't even be necessary. It has the system-specific type you defined in your struct's definition already.
What is important is that at the call site, you ensure that whatever you pass into the parameter has the matching type.
To me it looks like your closure is stored in a variable that has the same name as the parameter label gives, which might be a bit confusing in this case.

Wherever you create a Handlers instance, can you do it like this?

#if os(macOS)
    let myClosureParam = { (Bool) -> Void in
        // whatever you implemented in contentChanged
    }
#elseif os(iOS)
    let myClosureParam = { (String, String, Bool) -> Void in
        // whatever you implemented in contentChanged
    }
#endif
let handlers = Handlers(contentChanged: myClosureParam)

On a side note: Compiler directives are not simple text replacements, so you cannot put them inside, for example, initializer calls like this:

Handlers(contentChanged:
#if os(macOS)
    myClosureParam
#elseif os(iOS)
    myClosureParam
#endif
)

That's not needed anyway, because myClosureParam has to have the correct type anyways already!

2 Likes

This works, thanks!
But I still don't understand why the compiler forces me to use three arguments in this if else, when for macOS it should match the first if only. This really seems like a bug to me.

Not at all a bug. As already explained, in your original example, you conditionalized what functions you call in the body of the closure on the platform. The closure has to have some type, however—which is not conditionalized on the platform, and which (of course) doesn't depend on what functions you call in the body of the closure.

I think the problem here is more subtle--the type of a closure doesn't depend on what functions you call in the body (modulo return type inference), but it does depend on how many anonymous arguments you use within the body. There's no inherent problem AFAIK with the closure type checking differently on different platforms. In the past it's been suggested that we should allow 'number of closure parameters' to be inferred from context, in which case there would be no problem with:

Handlers(
    contentChanged: {} // one param on macOS, three on iOS
)

But it appears that what's happening here is that the check for 'highest anonymous parameter index' (which for a closure without explicit parameters is determinative for 'how many parameters does this closure have') is happening before we resolve conditional compilations. So even in the macOS case the compiler sees that the body of the closure 'mentions' $2 and therefore assumes the closure has three parameters. I expect the anonymous params are resolved at parse time?

5 Likes

@miku1958 Glad you can get it to work, but perhaps it helps you in the future to see that it is not a bug, but unavoidable, actually. :smile: I'll try to explain it a bit further, but that means I am going to make assumptions about the code outside of what you posted here (side note: totally understandable and good practice to not clutter everything with what you had and instead providing a minimal example illustrating the issue).

@Jumhyn while it could be interesting to make the "sugared" syntax even smarter at inferring the parameters, it probably won't ever resolve the issue @miku1958 encountered here, as what they seem to have written kind of "explicitly" results in a conflict.

Handlers(
    contentChanged: { // 1
#if os(macOS)
        contentChanged($0) // 2a
#elseif os(iOS)
        contentChanged($0, $1, $2) //2b
#endif
    },
)

1: Before the { is the label "contentChanged". That comes from the (probably) synthesized init of the Handlers struct. After the { begins the closure. The closure's type is also inferred from the Handlers's init (or rather, its property contentChanged). It has (Bool) -> Void type on macOS and (String, String, Bool) -> Void on iOS. Let's call this closure "outer thunk" from now on for clarity.

2a: This "contentChanged" identifier has nothing to do with Handlers's properties or init. From what I can see in the example given I must assume it is a variable that comes from the calling context of the initializer! It just happens to also be named "contentChanged", but it could be named differently. What type this closure has is not clear in the example. Just because it is called in an #if os(macOS) block that does not mean it has the same type as the similarly named property of Handlers! I am not sure which part of the shown error message refers to here or maybe the "outer thunk" closure as this init call is broken over several lines and I can simply not see enough in the screenshot. Regardless, as written in the example above the screenshot, here, on macOS, this closure variable is called with the first and only parameter of the "outer thunk" closure that is defined inline (beginning with the {).

2b: Similarly, this "contentChange" also has nothing to do with the Handlers type itself, but apparently is defined outside this context. On iOS it is called with three parameters that come from the "outer thunk" closure defined inline.

My hunch is that the variable contentChanged is defined without any compiler directives and is supposed to match the type of the Handlers property contentChange (once more: it's two different things with the same name! They live in different scopes!). Since it compiles on iOS I deduce it is defined as having the type (String, String, Bool) -> Void, regardless of platform, but @miku1958's intention is to have it defined differently on each platform.

In this case one does not need to have an "outer thunk" in the first place and can pass the variable directly to the parameter labeled contentChanged (I'd suggest to use a different name for the variable to make that clear). But you have to ensure the types match, by adding a compiler directive wherever the variable comes from. That's what I tried to relay with my earlier example. If it is a local variable defined right above the call to Handlers's init, that definition needs to be packed into an #if block accordingly, but other scenarios are possible.

Putting compiler directives inside "outer thunk" does not help at all, because that can only influence how it is called, but not which platform version.

This is not a bug, nor does it matter how smart the compiler can be at determining how many parameters there are for the $x syntax sugar. Once you start to define different type signatures for different platform, you have to "complete" the differences to make the calls match. Down the line, the logic is different, after all, right? iOS uses three parameters to do something, whereas macOS only uses one.

The only thing that might be improved on in this case, I guess, is the error messages or warnings, but I wouldn't know how and I think to judge that we need to see how that variable is defined. However, I think once you start including compiler directives in your type definitions these scenarios are unavoidable, as you're fundamentally introducing in effect two distinct implementations within a single file. Everything that then uses this has to respect it and make the same distinctions.

1 Like

I just simplified it in the example, the actual code looks like:

#if os(macOS)
func contentChanged(_ isEdited: Bool) {
  // do somthing
}
#elseif os(iOS)
func contentChanged(_oldValue: String, _ newValue: String, _ isEdited: Bool) {
  // do somthing
}
#endif

let handler = Handlers(
    contentChanged: { [weak self] in
#if os(macOS)
        self?.contentChanged($0)
#elseif os(iOS)
        self?.contentChanged($0, $1, $2)
#endif
    }
)
1 Like

Don't forget about the ability to return different types from a closure per-platform. That's the best solution I know of.

let handler = Handlers(
  contentChanged: { [weak self] in
#if os(macOS)
    { self?.contentChanged($0) }
#elseif os(iOS)
    { self?.contentChanged(_oldValue: $0, $1, $2) }
#endif
  } ()
)
3 Likes

Looks clear! Thank you so much!

1 Like

Wouldn't that capture [weak self] at the wrong level? It should be

let handler = Handlers(
  contentChanged: {
#if os(macOS)
    { [weak self] in self?.contentChanged($0) }
#elseif os(iOS)
    { [weak self] in self?.contentChanged(_oldValue: $0, $1, $2) }
#endif
  }()
)
1 Like

I played around with this a bit in a Playground and have to apologize to you, @miku1958. I had done so before, but wasn't able to reproduce it, so I assumed you had forgotten to properly type whatever you passed into the init of Handlers. While we have good workarounds now, it might be worthwhile to escalate this further.

I now do believe there is a potential bug, definitely it is an unfortunate edge case in how the compiler infers closure types from $x shorthand syntax inside blocks of compiler directives.

For completeness sake, I put several versions that hopefully illustrate this in my playground, you should be able to just copy & paste the following code into one and see for yourselves:

Playground code

You can simply comment and uncomment each section here, this shows what works and what not. See my comments under the second part for what I would consider an unexpected compiler behavior.

I just used DEBUG as a directive as it doesn't really matter what compiler directive exactly is used.

import Cocoa


// ###################################
// #### BASELINE ##################### --- WORKING
// ###################################

struct Handlers {
#if DEBUG
    var contentChanged: (Bool) -> Void
#elseif !DEBUG
    var contentChanged: (String, String, Bool) -> Void
#endif
}

class SomeClass {

#if DEBUG
    func contentChanged(_ theBool: Bool) -> Void {
        print("non-debug func with one Bool: \(theBool)")
    }
#elseif !DEBUG
    func contentChanged(_ stringOne: String, _ stringTwo: String, _ theBool: Bool) -> Void {
        print("debug func with stringOne: \(stringOne) - stringTwo: \(stringTwo) - theBool: \(theBool)")
    }
#endif

    func bla() {
        let handlers = Handlers(
            contentChanged: { [weak self] in
#if DEBUG
                self?.contentChanged($0)
#elseif !DEBUG
                self?.contentChanged($0, $1, $2)
#endif
            }
        )
    }
}



// ###################################
// #### REVERSED DIRECTIVES ########## --- NOT WORKING
// ###################################

//struct Handlers {
//#if !DEBUG
//    var contentChanged: (Bool) -> Void
//#elseif DEBUG
//    var contentChanged: (String, String, Bool) -> Void
//#endif
//}
//
//class SomeClass {
//
//#if !DEBUG
//    func contentChanged(_ theBool: Bool) -> Void {
//        print("non-debug func with one Bool: \(theBool)")
//    }
//#elseif DEBUG
//    func contentChanged(_ stringOne: String, _ stringTwo: String, _ theBool: Bool) -> Void {
//        print("debug func with stringOne: \(stringOne) - stringTwo: \(stringTwo) - theBool: \(theBool)")
//    }
//#endif
//
//    func bla() {
//        let handlers = Handlers( // Error 1, see below
//            contentChanged: { [weak self] in // Errors 2 & 3 and LOI 1, see below
//#if !DEBUG
//                self?.contentChanged($0)
//#elseif DEBUG
//                self?.contentChanged($0, $1, $2) // LOI 2, see below
//                print("only use $0: \($0)")
//#endif
//            }
//        )
//    }
//}
// Error 1: "Cannot convert value of type '(Bool, _, _) -> Void' to expected argument type '(Bool) -> Void'"
// Error 2: "Cannot infer type of closure parameter '$1' without a type annotation"
// Error 3: "Cannot infer type of closure parameter '$2' without a type annotation"
// LOI (line of interest) 1: Apparently the compiler interprets the closure in this LOI 1 as having 3 parameters
//                           because LOI 2 contains "$1" and "$2". This is reflected in the type signature error 1
//                           gives, where the types for these parameters is given as "_" for each. If you comment
//                           out LOI 2 the error goes away, but that prevents using them, obviously.
//                           The potential bug is that the compiler does not ignore "$1" and "$2" even though they
//                           are currently not really "visible" due to the compiler directive.



// ###################################
// # REVERSED DIRECTIVES 2: CLOSURES # --- WORKING
// ###################################

//struct Handlers {
//#if !DEBUG
//    var contentChanged: (Bool) -> Void
//#elseif DEBUG
//    var contentChanged: (String, String, Bool) -> Void
//#endif
//}
//
//class SomeClass {
//
//#if !DEBUG
//    func contentChanged(_ theBool: Bool) -> Void {
//        print("non-debug func with one Bool: \(theBool)")
//    }
//#elseif DEBUG
//    func contentChanged(_ stringOne: String, _ stringTwo: String, _ theBool: Bool) -> Void {
//        print("debug func with stringOne: \(stringOne) - stringTwo: \(stringTwo) - theBool: \(theBool)")
//    }
//#endif
//
//    func bla() {
//        let handlers = Handlers(
//            contentChanged: {
//#if !DEBUG
//                { [weak self] in self?.contentChanged($0) }
//#elseif DEBUG
//                { [weak self] in self?.contentChanged($0, $1, $2) }
//#endif
//            }()
//        )
//    }
//}
// This is a good workaround, especially if you need a thunk to capture self as weak (i.e. you cannot pass the
// function directly to avoid retain cycles.


// ###################################
// #### FULL CLOSURE SYNTAX ########## --- WORKING
// ###################################

//struct Handlers {
//#if DEBUG
//    var contentChanged: (Bool) -> Void
//#elseif !DEBUG
//    var contentChanged: (String, String, Bool) -> Void
//#endif
//}
//
//class SomeClass {
//
//#if DEBUG
//    func contentChanged(_ theBool: Bool) -> Void {
//        print("non-debug func with one Bool: \(theBool)")
//    }
//#elseif !DEBUG
//    func contentChanged(_ stringOne: String, _ stringTwo: String, _ theBool: Bool) -> Void {
//        print("debug func with stringOne: \(stringOne) - stringTwo: \(stringTwo) - theBool: \(theBool)")
//    }
//#endif
//
//    func bla() {
//        let handlers = Handlers(
//            contentChanged: { [weak self] (sOne: String, sTwo: String, theBool: Bool) -> Void in
//#if DEBUG
//                self?.contentChanged(theBool)
//#elseif !DEBUG
//                self?.contentChanged(sOne, sTwo, theBool)
//#endif
//            }
//        )
//    }
//}



// ###################################
// # FULL SYNTAX REVERSED DIRECTIVES # --- WORKING, SHOULD IT?
// ###################################

//struct Handlers {
//#if !DEBUG
//    var contentChanged: (Bool) -> Void
//#elseif DEBUG
//    var contentChanged: (String, String, Bool) -> Void
//#endif
//}
//
//class SomeClass {
//
//#if !DEBUG
//    func contentChanged(_ theBool: Bool) -> Void {
//        print("non-debug func with one Bool: \(theBool)")
//    }
//#elseif DEBUG
//    func contentChanged(_ stringOne: String, _ stringTwo: String, _ theBool: Bool) -> Void {
//        print("debug func with stringOne: \(stringOne) - stringTwo: \(stringTwo) - theBool: \(theBool)")
//    }
//#endif
//
//    func bla() {
//        let handlers = Handlers(
//            contentChanged: { [weak self] (theBool: Bool) -> Void in // LOI 1
//#if !DEBUG
//                self?.contentChanged(theBool)
//#elseif DEBUG
//                // I understand the compiler does ignore this atm, but should it?
//                self?.contentChanged(sOne, sTwo, theBool) // LOI 2
//#endif
//            }
//        )
//    }
//}
// It is remarkable that the compiler seems to "ignore" LOI 2 (the compiler directive makes it), even though
// the similar line in the "REVERSED DIRECTIVES" example above is not ignored when it comes to infer the type of
// the closure counterpart to LOI 1. This is surprising, especially since this explicit case here definitely
// won't compile if the compiler directive's "switch".

No. Weakery propagates, hence the need for optional chaining. After you've shadowed and captured, there's no way to get access to a strong reference that I know of.

@Test func captures() {
  var strong = Class(weak: true) as _?
  weak var weak = strong

  strong = nil
  #expect(weak == nil)

  strong = .init()
  weak = strong
  strong = nil
  #expect(weak != nil)
}

final class Class {
  var closure: (() -> Void)!
  func function() { }

  init(weak: Bool = false) {
    closure = if weak {
      { [weak self] in
        { self?.function() }
      } ()
    } else {
      function
    }
  }
}