After my recent exposure to Python, I learned to appreciate the readability of its conditional expressions with the form of X if C else Y
.
When the classical ternary conditional operator ?: has the condition as first argument, it feels like the assignment is all about the choice. It gets ugly when you try nesting multiple ternary operators… When the condition is moved after the first expression, it feels more like a mathematical definition of a function. I find this sometimes helps with code clarity.
As code kata, I wanted to implement python style conditional expression in Swift. It seems like the only tool that can get me the required syntax are custom operators. They must be composed of symbols. Math symbols in Unicode label the double turnstile symbol from logic as TRUE ⊨
and crossed out version is NOT TRUE ⊭
… so I went with ==|
and |!=
. So far, after fighting for a while to find the correct precedence, I have this:
// Python-like conditional expression
struct CondExpr<T> {
let cond: Bool
let expr: () -> T
}
infix operator ==| : TernaryPrecedence // if/where
infix operator |!= : TernaryPrecedence // else
func ==|<T> (lhs: @autoclosure () -> T, rhs: CondExpr<T>) -> T {
return rhs.cond ? lhs() : rhs.expr()
}
func |!=<T> (lhs: Bool, rhs: @escaping @autoclosure () -> T) -> CondExpr<T> {
return CondExpr<T>(cond: lhs, expr: rhs)
}
I know the result doesn't look very swifty or particularly readable, but on the bright side, these operators work even when the expression is spread over multiple lines.
let e = // is 12 (5 + 7)
1 + 3 ==| false |!=
5 + 7 ==| true |!=
19 + 23
When you get creative with whitespace, it even feels a bit pythonic 🐍 :
let included =
Set(filters) ==| !filters.isEmpty |!=
Set(precommitTests.keys) ==| onlyPrecommit |!=
Set(allTests.map { $0.key })
I don't like that the second autoclosure has to be escaping. Nate Cook's answer about custom ternary operators in Swift 2 used currying syntax, which is no longer in Swift 3… and I guess that was technically also an escaping closure.
Is there a way to make this work without escaping closure? Does it even matter? Maybe the Swift compiler is smart enough to resolve this during compile time, so that it has no runtime impact?
Great question :)
Rather than storing the expression, store the result as an optional if the conditional is false. Edited based on some comments, resulting in some much cleaner code.
infix operator ==| : TernaryPrecedence // if/where
infix operator |!= : TernaryPrecedence // else
func ==|<T> (lhs: @autoclosure () -> T, rhs: T?) -> T {
return rhs ?? lhs()
}
func |!=<T> (lhs: Bool, rhs: @autoclosure () -> T) -> T? {
return lhs ? nil : rhs()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With