Pitch idea: Compiler warnings about explicit vs implicit returns
Motivation
Swift’s handling of implicit vs explicit return in single-expression functions and closures can lead to ambiguity—especially in the presence of @discardableResult and loosely typed closures.
Problem
Two related issues stand out:
- Ambiguity with
@discardableResult
A function returning a value (e.g.Int) but marked@discardableResultcan behave differently depending on context:- In a closure typed as
Void, it may be treated as a discarded result (sometimes with a warning). - In an untyped closure, it may silently become the return value. This makes seemingly small changes (e.g. adding
items.removeLast()) alter behavior in non-obvious ways.
- In a closure typed as
- Lack of explicit-return tooling support
While Swift supports omittingreturn, there is currently no symmetrical, compiler-supported way to prefer explicit returns, and linter support is problematic in type imcoplete or ambiguous cases like the above. This leaves teams without a reliable way to enforce intent when they want clarity over brevity.
Proposal
- Introduce a new compiler-supported option with three modes: default (current behavior), “prefer omitted returns” (similar to today’s linters in limited contexts), and “prefer explicit returns” (a new capability not currently supported by linters).
- Make the compiler aware of
@discardableResultwhen deciding return behavior, preferring explicitreturnto clarify intent. - Ensure consistent behavior across:
- functions
- closures (properly typed, mistyped, untyped)
- (future) multi-expression bodies
Why Compiler-Level
Linters cannot reliably detect @discardableResult across file or module boundaries. The compiler already has this semantic knowledge (and uses it for unused result warnings), making it the correct place to enforce or guide this behavior.
Benefit
- Eliminates subtle, context-dependent behavior
- Makes intent explicit when it matters
- Provides symmetry between “omit return” and “prefer explicit return” styles
A table comparing current behaviour and proposed modes is attached below where
indicates success,
- a warning, and
- an error.
@discardableResult func foo() -> Int { 0 }
func bar() -> Int { 0 }
// OFF (AS TODAY)
// | PREFER IMPLICIT RETURNS (OMIT RETURNS IF POSSIBLE)
// | | PREFER EXPLICIT RETURNS (ALWAYS PUT RETURNS)
// ↓ ↓ ↓
func baz1() -> Int { foo() } // ✅ 🔶 🔶 (OK | need return for clarity | want return)
func baz2() -> Int { bar() } // ✅ ✅ 🔶 (OK | OK | want return)
func baz3() -> Int { return foo() } // ✅ ✅ ✅ (OK | OK | OK)
func baz4() -> Int { return bar() } // ✅ 🔶 ✅ (OK | unwanted return | OK)
func baz5() -> Void { foo() } // ✅ 🔶 🔶 (OK | need return for clarity | want return)
func baz6() -> Void { bar() } // 🔶 🔶 🔶 (unused result | unused result | want return
func baz7() -> Void { return foo() } // ❌ ❌ ❌ (wrong type | wrong type | wrong type)
func baz8() -> Void { return bar() } // ❌ ❌ ❌ (wrong type | wrong type | wrong type)
let baz9 = { foo() } // ✅ 🔶 🔶 (OK | need return for clarity | want return)
let baz10 = { bar() } // ✅ ✅ 🔶 (OK | OK | want return)
let baz11 = { return foo() } // ✅ ✅ ✅ (OK | OK | OK)
let baz12 = { return bar() } // ✅ 🔶 ✅ (OK | unwanted return | OK)
let baz13: () -> Int = { foo() } // ✅ 🔶 🔶 (OK | need return for clarity | want return)
let baz14: () -> Int = { bar() } // ✅ ✅ 🔶 (OK | OK | want return)
let baz15: () -> Int = { return foo() } // ✅ ✅ ✅ (OK | OK | OK)
let baz16: () -> Int = { return bar() } // ✅ 🔶 ✅ (OK | unwanted return | OK)
let baz17: () -> Void = { foo() } // ✅ 🔶 🔶 (OK | need return for clarity | want return)
let baz18: () -> Void = { bar() } // 🔶 🔶 🔶 (unused result | unused result | want return)
let baz19: () -> Void = { return foo() } // ❌ ❌ ❌ (wrong type | wrong type | wrong type)
let baz20: () -> Void = { return bar() } // ❌ ❌ ❌ (wrong type | wrong type | wrong type)