Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python style conditional expression in Swift 3

Tags:

swift

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?

like image 731
Palimondo Avatar asked May 04 '17 21:05

Palimondo


1 Answers

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()
}
like image 88
Gary Makin Avatar answered Sep 19 '22 08:09

Gary Makin