() / (()) conversion in curried generic input types

Edit: I deleted a bunch of stuff because I was getting answers about something other than the title.

Given all this:

func ƒ0(_: () -> Void) { }
func ƒT<T>(_: (T) -> Void) { }

func ƒ00(_: () -> () -> Void) { }
func ƒ0T<T>(_: () -> (T) -> Void) { }

func makeClosure() -> () -> Void { { } }
func makeWeirdClosure() -> (()) -> Void { { _ in } }

Why do these compile…

ƒ0 { }
ƒT { }
ƒ00 { { } }
ƒ00(makeClosure)
ƒ0T(makeWeirdClosure)

…but not these?

ƒ0T { { } }
ƒ0T(makeClosure)

Is there no way to avoid extra-tupling with curried inputs?

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)
(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)
(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)
(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)
(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

What's going on here?

2 Likes

The post has been changed so much that the answers did not reflect anything useful anymore.

I would really like for Swift to just automatically allow a function f(Void) -> T to be callable as f() instead of f(()). This is truly annoying when using generics. I have no idea how often I've had to write stuff like this:

public extension Result where Success == Void {
    static var success: Result { return .success(()) }
}

Just so I can write return .success instead of return .success(())

Or this (which also deals with the fact that Void isn't hashable):

public enum Unit: Hashable { case unit }
public extension Cache where Key == Unit {
    typealias VoidGenerator = () -> Value

    convenience init(_ generator: @escaping VoidGenerator, maxAge: DispatchTimeInterval = .never) {
        self.init(PromiseCache.lift(generator), maxAge: maxAge)
    }
    func getIfNeeded(forceReload: Bool = false) -> Value {
        return getIfNeeded(.unit, forceReload: forceReload)
    }
    var cachedValue: Value? {
        return self[.unit]
    }
    func purge() {
        purge(.unit)
    }
    private static func lift(_ generator: @escaping VoidGenerator) -> Generator {
        return { (_: Unit) in generator() }
    }
}

Just to allow me to use Void as a "key" even though it's not hashable, and then allow me to write .getIfNeeded() instead of .getIfNeeded(.unit) or .getIfNeeded(())

1 Like

Wouldn't that have strange implications or at least lead to questions like these:

protocol P {
    associatedtype A
    func foo(_ v: A) -> A
}
struct S: P {
    func foo() { print(A.self) } // Should this satisfy P's requirement and print Void?
}

func foo<T>(_ a: T, _ b: T) -> (T, T) { return (a, b) }
foo() // Should this compile and print ((), ())?

typealias A = () -> Void
typealias B = (Void) -> Void
typealias C<T> = (T) -> Void
print(A.self) // () -> ()
print(B.self) // (()) -> () but should be () -> ()?
print(C<Void>.self) // (()) -> () but should be () -> ()?
print(A.self == B.self) // false, but should be true?
print(A.self == C<Void>.self) // false, but should be true?
print(B.self == C<Void>.self) // true, but should be false?

Probably. I don't know. As per usual there is probably some reason I can't have what I want. (And a reason I'm not a compiler engineer or language designer) ¯\_(ツ)_/¯

Don't we have similar ambiguities elsewhere in the language? E.g we allow for a non-optional to be passed, where an optional is expected. And now to pass a keypath literal where a function is expected. But we still have

Optional<T>.self == T.self // false
((A) -> B).self == KeyPath<A, B>.self // false

Idk. Maybe we could allow for func f(Void) -> () to be a candidate for call site f() but with a lower overload priority score than func f()?

The OP is asking about an inconsistency, and I'd like to focus on the first inconsistency demonstrated by the example.

The type of ƒ0 is:

((  ) -> Void) -> Void

The type of ƒT<Void> is:

((()) -> Void) -> Void

So, ƒ0 takes an argument of type () -> Void, and
ƒT<Void> takes an argument of type (()) -> Void.

Ie, the functions are of different types (because they take arguments of different types), as demonstrated by the following code:

let f: ((  ) -> Void) -> Void = ƒ0
let g: ((()) -> Void) -> Void = ƒT
print(type(of: f)) // (() -> ()) -> ()
print(type(of: g)) // ((()) -> ()) -> ()
print(type(of: f) == type(of: g)) // false

let a: (  ) -> Void = { }
let b: (()) -> Void = { _ in }
print(type(of: a)) // () -> ()
print(type(of: b)) // (()) -> ()
print(type(of: a) == type(of: b)) // false

// OK, so f takes a, and g takes b, but look here:

f(a) // Compiles (as expected)
g(b) // Compiles (as expected)
f(b) // ERROR (as expected): Cannot convert value of type '(()) -> Void' to expected argument type '() -> Void'
g(a) // Compiles (... but why?)

I'd expect this:

f(a) // Compiles
g(b) // Compiles
f(b) // ERROR: Cannot convert value of type '(()) -> Void' to expected argument type '() -> Void'
g(a) // ERROR: Cannot convert value of type '() -> Void' to expected argument type '(()) -> Void'
1 Like

This “inconsistency” is the deliberate exception to SE-0110 described here:

https://lists.swift.org/pipermail/swift-evolution-announce/2017-June/000386.html

2 Likes

So that's the answer to the question of the OP then, that this exception is ... well, an exception :slight_smile: (that thus doesn't apply consistently through the example of the OP).

As is true for all exceptions, they are motivated by usability / ergonomics in some context, but they always pop up as a confusing and/or frustrating inconsistency in some other context.

I wish Swift had fewer exceptions like this.

1 Like

Thankfully, I don't have to write this:

let promise: Promise<Void, Error> = ...

promise.onSuccess { _ in // <- void argument 
   // do something
}

But can write this instead:

promise.onSuccess {
   // do something
}

However, I still would want f() to consider func f(_: Void) as a candidate.

Thanks for digging that up!

Now it's safe to repost the relevant part of the use case. I'd like to avoid having to extend this:

struct WeakMethod<Reference: AnyObject, Input, Output> {
  typealias Method = (Reference) -> (Input) -> Output

  weak var reference: Reference?
  var method: Method

  struct ReferenceDeallocatedError: Error { }

  /// - Throws: ReferenceDeallocatedError
  func callAsFunction(_ input: Input) throws -> Output {
    guard let reference = reference
    else { throw ReferenceDeallocatedError() }

    return method(reference)(input)
  }
}

…with this:

extension WeakMethod where Input == () {
  init(
    reference: Reference?,
    method: @escaping (Reference) -> () -> Output
  ) {
    self.reference = reference
    self.method = { reference in
      { _ in method(reference)() }
    }
  }

  /// - Throws: ReferenceDeallocatedError
  func callAsFunction() throws -> Output {
    try self( () )
  }
}

Without that extension's initializer, this won't compile:

final class Reference {
  func method() { }
}

var reference: Reference? = .init()
let method = WeakMethod(reference: reference, method: Reference.method)

I think it's incorrect for that Input to be () there, but the empty tuple is the closest representation the type system has to nothingness. I can understand why I need the extension method, to allow for this:

try method()

…but the initializer being required comes off as really weird, to me, and the problem didn't come along with a helpful error message, either. I think Svein is onto something. Nested zero-tuples should probably always be condensed to nothingness.

1 Like

This was the case for earlier versions of Swift. It is an explicit design decision of SE-0110 that this not be the case.

…which means that it's impossible to represent

let voidClosure: ( () -> Void ).Type

with any form of this:

typealias Closure<Input> = (Input) -> Void

…even though it works with every other type?

let intClosure: ( (Int) -> Void ).Type = Closure.self
let neverClosure: ( (Never) -> Void ).Type = Closure.self
let horribleClosure: (  ( () ) -> Void  ).Type = Closure.self

Yes, the generic parameter represents exactly one type (not zero, not two or more). See somewhat related discussion in this post and replies.

1 Like

So variadic generics will solve it? :smiley_cat:

Here is another thread that might be relevant:

If was proposing making

func f<T>() -> T { /*...*/ }

sugar for:

func f<T>(_: Void = () ) -> T { /*...*/ }

During Swift 4.0 to 4.1 transition. It meant functions would have from 1 to n arguments, instead of 0 to n.

The argument for it was reducing source breakage resulting from the removal of tuple splat. I was in favor and didn't really understand what was the valid argument against it.

1 Like
Terms of Service

Privacy Policy

Cookie Policy