Off the cuff, here is an example that applies De Morgan’s law to an expression using a hypothetical AST library:
func deMorganTransform(of expr: Expression) -> Expression {
// Match `!(y <op> z)`
guard
let expr = expr as? UnaryExpression,
expr.operator == .negation,
let subexpr = expr.operand as? BinaryExpression
else {
return expr
}
// Look for `&&` or `||` in subexpr; anything else
// leaves the whole original expression intact
let negatedOperator: BinaryExpression.Operator =
switch subexpr.operator {
case .and: .or
case .or: .and
default:
return expr // ✅
}
return BinaryExpression(
operator: negatedOperator,
lhs: UnaryExpression(.negation, operand: subexpr.lhs),
rhs: UnaryExpression(.negation, operand: subexpr.rhs))
}
Without the mid-case return marked , the code has to undertake some mild gymnastics: either…
- …repeat the whole final return statement twice (once each for the
and
andor
cases), - …or add a (maybe nested) helper function to factor out that repetition,
- …or make
negatedOperator
optional, and then have two separate return cases afterwards, - …or etc.
The alternatives are all much less elegant. Not that this is the only way to write this function, but…it’s a reasonable one, and the mid-expression return
makes it a much better one.
It’s also possible to construct examples using nested if
/else
trees, especially using if let
bindings, where a mid-expression return is tidier than having to propagate the special case all the way to the top level of the expression and then deal with it.
The basic principle here is much like the failable initializer: a special case encountered mid-expression aborts the whole operation, and it’s easier to handle that special case where it occurs rather than create a forking path later.
(@Douglas_Gregor, I know you’re skeptical; am I swaying you at all?)