[Pitch] Enable multi-statement closure parameter/result type inference

I found a case with source compatibility implications:

func test<T, R>(_: (UnsafePointer<T>) -> R) -> R { fatalError() }
func test<T>(_: (UnsafeRawBufferPointer) -> T) -> T { fatalError() }

let _: Int = test { ptr in
  //assert(ptr[0] == UInt8.max)
  return Int(ptr[0]) << 2
}

Without assert this closure is ambiguous but with it - type-checks just fine, but only if I specify contextual type explicitly. This happens because overload with UnsafeRawBufferPointer doesn't have any unresolved type variables in it (due to supplied contextual result type), so it's the only valid solution for multi-statement closure case without checking the body.

With proposed changes this test-case is going to be ambiguous regardless of assert because it would be possible to infer pointer type from the body of the closure.

I'm going to add this to source compatibility section and it should be possible to mitigate via ranking rules if we wanted but I'd prefer to keep the unified behavior.

I think some of this is style preference, but I personally would rather not go down this path. I find a bare expression at the end of some other statements being an implied return very unsettling:

_ = vals.map {
  if case .baz(let value) = $0 {
    return value
  }
  nil
}

I know this is how several languages work but I just don't like it. I'd have to think about how to articulate why.

I think something that will go a long way to making it less pressing would be if if/switch statements became expressions. IIRC @Michael_Ilseman went through an analysis of the standard library a while back and found that a huge number of functions would benefit from this.

_ = vals.map {
  if case .baz(let value) = $0 {
    value
  } else {
    nil
  }
}

(this doesn't mesh as well with guard of course, though I feel like guard is a little overused tbh)

Having raised this topic... I'll also say we shouldn't discuss it on this pitch, except in so much as to say this proposal's implementation makes this change very simple. But I suggest we discuss it on a separate thread.

9 Likes

Currently statements in any body are type-checked top-down, that's why first return provides the type, changing that would require the same mechanism as I have described for return type elision, where information is allowed to flow both directions (and result is a join) and across statements which would be new for the language. More importantly type-checking would not be top-down anymore.

2 Likes

Just a quick update: I have updated both Source Compatibility and Future Directions sections based on the discussion. Future Directions how has a topic about return type inference across statements.

4 Likes
@discardableResult func a() -> Int { 42 }
let c = {
    _ = 1 // any statement
    a() 
}
let d = c()

A convoluted example, but wouldn’t d change from Void to Int?

I think this is more of an unfortunate semantic on part of a single-expression closure because @discardableResult shouldn't infer as an implicit return...