Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift @autoclosure parameter wraps provided explicit closure

Tags:

closures

swift

Consider the following function:

func whatever(foo: @autoclosure () -> Int) {
  let x = foo()
  print(x)
}

Naturally, we can invoke it like this:

whatever(foo: 5)
// prints: 5

However providing an explicit closure argument causes the compiler to complain:

whatever(foo: { 5 })
// Error: Function produces expected type 'Int'; did you mean to call it with '()'?

Is this the intended? Reading the documentation for @autoclosure I did not find a statement about whether arguments are always wrapped, even when providing a closure. My understanding of @autoclosure was:
Take a closure argument. If the argument is not a closure but has the same type as the closure would return, wrap it.
However, the behaviour I'm seeing is: Wrap the argument no matter what.

A more elaborate example makes this seem very odd to me:

struct Defaults {

  static var dispatcher: Defaults = ...

  subscript<T>(setting: Setting<T>) -> T { ... }

  struct Setting<T> {
    let key: String
    let defaultValue: () -> T

    init(key: String, defaultValue: @escaping @autoclosure () -> T) {
      self.key = key
      self.defaultValue = defaultValue
    }
  }
}

extension Defaults.Setting {
  static var nickname: Defaults.Setting<String> {
    return Defaults.Setting(key: "__nickname", defaultValue: "Angela Merkel")
  }
}

//  Usage:
Defaults.dispatcher[.nickname] = "Emmanuel Macron"

Now let's say I want to hash the key of a Setting value:

extension Defaults.Setting {
  var withHashedKey: Defaults.Setting<T> {
    return Defaults.Setting(key: key.md5(), defaultValue: defaultValue)
    // Error: Cannot convert return expression of type 'Defaults.Setting<() -> T>' to return type 'Defaults.Setting<T>'
  }
}

To clarify: defaultValue is of type () -> T. Providing it to init(key: String, defaultValue: () -> T), in my expectation should just work, because the argument and parameter have the same type (while parameter is @autoclosure).
However, Swift seems to wrap the provided closure, effectively creating () -> () -> T, which creates Setting<() -> T> instead of Setting<T>.

I can work around this issue by declaring an init which takes an explicitly non-@autoclosure parameter:

extension Defaults.Setting {
  init(key: String, defaultValue: @escaping () -> T) {
    self.init(key: key, defaultValue: defaultValue)
  }
}

What's really daunting is that I can simply forward to the init taking the @autoclosure parameter and it works.

Am I missing something here or is it just not possible by design in Swift to provide closure arguments to @autoclosure parameters?

like image 865
CodingMeSwiftly Avatar asked Oct 24 '25 19:10

CodingMeSwiftly


1 Answers

Swift expects you to pass an expression that results in an Int to whatever(foo:) and Swift will wrap that expression in a closure of type () -> Int.

func whatever(foo: @autoclosure () -> Int) {
    let x = foo()
    print(x)
}

When you call it like this:

func whatever(foo: {5})

you are passing an expression that results in () -> Int and not the Int Swift expects. That is why it suggests you add () and call that closure to get an expression that returns an Int:

func whatever(foo: {5}())

Note that since Swift wraps {5}() in a closure, it does not get evaluated before the call to whatever(foo:) but in fact the call is delayed until you evaluate let x = foo().

You can verify this by running this in a Playground:

func whatever(foo: @autoclosure () -> Int) {
    print("inside whatever")
    let x = foo()
    print(x)
    let y = foo()
    print(y)
}

whatever(foo: { print("hi"); return 3 }())

Output:

inside whatever
hi
3
hi
3

If you want whatever(foo: to also be able to take a () -> Int closure, overload it and call the autoclosure version after calling foo:

func whatever(foo: @autoclosure () -> Int) {
    print("autoclosure whatever")
    let x = foo()
    print(x)
}

func whatever(foo: () -> Int) {
    print("closure whatever")
    whatever(foo: foo())

}

whatever(foo: { print("two"); return 6 })
whatever(foo: { print("two"); return 6 }())

Output:

closure whatever
autoclosure whatever
two
6
autoclosure whatever
two
6
like image 103
vacawama Avatar answered Oct 26 '25 07:10

vacawama