This pitch has not yet been formalised into a proposal. Given enough support, I will convert this into a correctly formatted proposal document and (hopefully) provide an implementation.
Introduction
The switch and catch statements contain an if-like pattern using where. This should support every feature available to if, including binding variables with if let and if case. This proposal aims to include these possibilities within the grammar.
Motivation
In a switch statement, the grammar only allows for expressions evaluating to Bool after where:
enum A {
case one
case two
case three
}
enum B {
case string(String)
case number(Int)
}
let a = A.one
let b = B.string("foo")
let s = "foo"
switch a {
case .one where s == "foo":
print("one")
case .two:
print("two")
case .three:
print("three")
}
but it is not possible to check what b is and bind it without another layer of indentation and repeating the code in the default case.
switch a {
case .one:
if case .string(let s) = b {
print("one and \(s)")
} else {
print("default")
}
case .two:
print("two")
default:
print("default")
}
A similar issue affects error handlers:
do {
// some error-throwing code
} catch ExampleError as e where e.string == "foo" {
// handle the error
} catch ExampleError as e {
// handle a less specific error
}
and it is not possible to check b without:
do {
do {
// some error-throwing code
} catch ExampleError as e where e.string == "foo" {
if case .string(let s) = b {
// handle the error
}
}
}
} catch ExampleError as e {
// handle a less specific error
}
Proposed Solution
This proposal aims to expand the case x where y: and catch x where y: syntax to include every feature offered by if and guard, that is, make the y part any condition-list instead (see the grammar for the definition). Thus the following code will become valid:
let b = .string("foo")
switch a {
case .one where case .string(let s) = b:
// matches when a is .one, then verifies b is .string
// and binds the string to s.
print("one and \(s)")
case .two:
print("two")
default:
print("default")
}
instead of the more cumbersome alternative given in the motivation section.
Detailed Design
Grammar changes
From the current grammar:
condition-list → condition | condition
,condition-list
condition → expression | availability-condition | case-condition | optional-binding-condition
case-condition →casepattern initializer
optional-binding-condition →letpattern initializer ? |varpattern initializer ?
(from the while grammar, where condition-list is defined)
where-clause →
wherewhere-expression
where-expression → expression
(from the switch grammar, also applies to where in catch statements)
There are no modifications included for the condition-list section. This proposal would redefine the where-expression section as below:
where-expression → condition-list
The original behaviour is still captured as condition-list can be replaced by condition which can then be replaced by expression, which is also required to evaluate to a boolean. This allows any condition valid in if, while and guard statements to be used in where.
Behaviour for switch
As the where keyword now uses condition-list, the terms case-condition, optional-binding-condition, availability-condition and expression can be stacked with ,:
let c: Int? = 5
let d: Int? = nil
switch a {
case .one where let c:
print("one and \(c + 1)")
case .two where let c:
print("two and \(c + 2)")
case .three where d == nil, let c, case .number(let n) = b:
// given that:
// a is .three, d is nil, c exists, b is .number (bind to n)
// execute the below code.
print("three, d is nil and \(c + n)")
default:
print("default")
}
Again, any variables or constants bound by var or let after where are handled following the binding of variables in the case preceding the where. Every part of the case is handled in the direction of writing (in English, left to right).
enum C {
case maybeInt(Int?)
case maybeStr(String?)
}
let b2 = C.maybeInt(2)
let b3 = B.number(5)
switch b2 {
case .maybeInt(let n) where case .number(let m) = b3, let realN = n:
// given that:
// b2 is .maybeInt (bind n: Int?), b3 is .number (bind m: Int), n exists
// run the code below.
// order is important for both sides of the where.
print(m * real_N)
case .maybeStr(let s) where case .string(let t) = b3, let realS = s:
print(m + real_S)
default:
print("default")
}
Alternatively, using the .maybeInt(let baz?) syntax:
switch b2 {
case .maybeInt(let n?) where case .number(let m) = b3:
// b2 is .maybeInt (bind n: Int if it exists), b3 is .number (bind m: Int)
// order is important for both sides of the where.
print(m * real_N)
case .maybeStr(let s?) where case .string(let t) = b3:
print(m + real_S)
default:
print("default")
}
Additions to fallthrough usage rules
Any additions to the list of variables/constants bound by the case can still prevent fallthrough to areas without access to these variables. The code below will therefore not compile:
switch b2 {
case .maybeInt(let n):
fallthrough
// error: fallthrough from a case which doesn't define realN, t
case .maybeInt(let n) where case .string(let t) = b3, let realN = n:
print(t + String(realN))
}
Additions to default rules
For this section:
enum E {
case a
case b
case c
}
enum F {
case a(Int)
case b(String)
case c(E)
}
let d = E.a
let f = F.a(1)
let option: X?
The cases where default in a switch is not needed will be extended as follows:
If there is a case .a where let option pattern,
- either the case
case .a where option == nil, (to handle the optional) - or
case .a(a case without thewhereclause)
must be included.
If there is a case .a where case E.c = d pattern:
- the cases
case .a where case E.a = dandcase .a where case E.b = d(for all enum cases), - or the case
case .a(a case without thewhereclause)
must be included.
For enumerated types with associated values, this extends like the case for switch itself:
If there is a case .a where case F.a(1) = f pattern:
- the case
case .a where case F.a(let x) = f, - or the case
case .a(a case without thewhereclause)
must be included.
If the specified value is one of an enumerated type, like case .a where case F.c(.a) = f:
- the case
case .a where case F.a(let x) = f, - or the cases
case .a where case F.a(.b) = f,case .a where case F.a(.c) = f(for all cases ofE) - or the case
case .a(a case without thewhereclause)
must be included.
For multiple conditions, there must be one case for each combination possible.
For structure checking, checking every alternative may be too much work in extreme circumstances. For a very large number N:
enum A1 {
case a(A2)
}
enum A2 {
case a(A3)
}
//... (from 1 to N)
enum AN {
case a(B)
}
enum B {
case one
case two
}
let x: A1 = .a(.a(.a(... .a(1)))) //... from 1 to N
switch x {
case .a(let a2) where case .a(.a(... .a(.one))) = a2: //... from 2 to N
print("B is 1")
case .a(.a(let a3)) where case .a(.a(... .a(.two))) = a3: //... from 3 to N
print("B is 2")
// no default required, takes a long time to show.
}
If showing that no default is required takes too long due to heavily nested case ... where case structures, a default case can be added. If a default case is present, the verification that no default is required does not happen under the current compiler. If a mistake was made and the default is reached, simply include a print statement:
switch a {
// long and confusing case sequence
default:
print("unexpectedly reached default case at line \(#line)")
}
Behaviour for catch
This is very similar as that of switch, with errors instead of enum cases.
As the where keyword now uses condition-list, the terms case-condition, optional-binding-condition, availability-condition and expression can be stacked with ,:
let c: Int? = 5
let d: Int? = nil
do {
// some error-throwing code
} catch ErrorOne where let c {
print("one and \(c + 1)")
} catch ErrorTwo where let c {
print("two and \(c + 2)")
} catch ErrorThree where d == nil, let c, case .number(let n) = b {
// given that:
// a is .three, d is nil, c exists, b is .number (bind to n)
// execute the below code.
print("three, d is nil and \(c + n)")
} catch _ {
print("default")
}
As above, any variables or constants bound by var or let after where are handled following the binding of variables in the case preceding the where. Every part of the catch is handled in the direction of writing (in English, left to right).
struct ErrorMaybeInt: Error {
var maybeInt: Int? = nil
}
struct ErrorMaybeStr: Error {
var maybeStr: String? = nil
}
let b3 = B.number(5)
do {
// some error-throwing code
} catch ErrorMaybeInt as e where
case .number(let m) = b3, let realN = e.maybeInt {
// given that:
// - thrown error is ErrorMaybeInt (bind n: Int?),
// - b3 is .number (bind m: Int), n exists
// run the code below.
// order is important for both sides of the where.
print(m * real_N)
} catch ErrorMaybeStr as e where
case .string(let t) = b3, let realS = e.maybeStr:
print(m + real_S
} catch _ {
print("default")
}
Additions for showing a do statement does not leak errors
As in the discussion for switch, the type-checker inferring that a catch sequence does not allow any errors through can be avoided with catch _ {break} or similar when it is clear that any error will be handled by a previous handler. This avoids the type-checker spending time trying to show that no errors can escape the block and can be done with a single line. If a mistake was made and the default is reached, simply include print statements:
do {
// long and confusing error-throwing code
} catch
// long and confusing catch sequence
} catch {
print("unexpectedly reached default catch statement at line \(#line)")
print("the error was \(error)")
}
If something truly terrible could happen as a result, fatalError may be appropriate.
Source Stability
The changes are purely additive, so no source code will be broken by this change.
The space of valid programs has been expanded.
Deeply nested structures may cause type-checking to slow, but this can be prevented by adding default: break to any extreme cases. This will not need to happen in existing code as the complications for showing that no default is required are introduced by this proposal's changes.
Interestingly, Swift's syntax highlighting (in Xcode at least) seems to already colour where case and where let statements correctly. The reason for this is unknown.
ABI Stability
This change affects only the type-checking and parsing of the language and makes no changes to the Standard Library. The changes to type-checking and parsing are purely additive, so there is no effect on existing code.
Implications of Adoption
As there is a change to the grammar, code using this feature cannot revert to the current grammar without significant change. The change will not affect existing code. The capacity to exponentially slow the compiler when checking that a default case is not required may cause problems for new code, but at present, default: break may be used to alleviate this.
Future directions
fallthrough
The modifications to fallthrough restrict its use cases when where case statements are used. Therefore a mechanism to avoid fallthrough errors where variables are defined in one case but not in another may be required.
Other condition statements
If any condition (as in the grammar) is added to the language in the future, if, guard, while and as of this proposal, where, will need to support this. A unified system for condition-list statements may be of benefit to the language design.
Similarly, any future conditional expression like where or if may be extended to support the whole condition-list range if this is feasible.
Unexpected default mark and test
An addition to the testing framework could be made to ensure that while tests are running, no specially marked default blocks are reached in either switch or do catch. An annotation to the default or catch _ could be added for this purpose, for example:
switch a {
// lots of cases
@unexpected default:
fatalError("unexpected default at \(#line) reached due to \(a)")
}
Alternatives Considered
No changes at all
These changes do not add any new expressibility to the language, as any where expression with a condition-list can be rewritten using if:
func defaultBehaviour() {} // the default case behaviour
func action(e: X,h: Y) {} // the action to be performed
// This code:
switch a {
case .one where c == d, let e = f, case .two(let h) = g:
action(e,h)
default:
defaultBehaviour()
}
// is equivalent to:
switch a {
case .one where c == d:
if let e = f, case .two(let h) = g {
action(e,h)
} else {
defaultBehaviour()
}
default:
defaultBehaviour()
}
There are some problems; the second sample:
- is less readable
- has more indents (3 compared to 2)
- repeats
defaultBehaviour()outside ofdefault.
This method does, however, shorten the line containingcase, but this is not as big of an issue compared to losing readability and repeating part of the code.
The let x? binding syntax
When an enumerated type includes an optional, let x? can be used to bind x when it is not nil. It is already confusing, as the ? gives the appearance of an optional being added, not removed. Extending this to checking where case would be even more confusing:
switch a {
case .one(let x?,let case .two(let z) = y):
//...
}
instead of the proposed syntax:
switch a {
case .one(let x, let y) where let x, case .two(let z) = y:
//...
}
The let x? and let case are, however, shorter. Shorter syntax is not preferable to more readable syntax, especially since the shortening is very minor.
Additional/other keywords
There are enough keywords allocated to express these statements, and where case seems to read nicely already. Having a different set for where would be confusing, and contravene the aim of this proposal: to bring parity to where,if,guard and while.
where in switch and not in do catch
Implementing this for only switch and not catch would leave an obvious improvement for another proposal, and would make the grammar unnecessarily complicated with two different where patterns.
Mandating the verification that cases or catch blocks are complete
From the earlier sections, the type-checker may be forced to check an exponentially growing structure to detect the requirement of a default case or catch. This can also be easily remedied by a tiny default block.
Acknowledgements
Thank you to Albert Stark for GitHub-related information.