Bringing Control Flow Keywords to the Ternary Operator

In Swift, the ternary operator easily allows us to clean and concisely write conditionally code. Although currently, the inability to use certain statements in conjunction with the ternary operator is limiting.

I am proposing the addition of 5 statements for usage in conjunction with the ternary operator:

All of these various statements will greatly improve the flexibility and ease of use with the ternary operator.

break


This will bring the use of the keyword break to the ternary operator.

This will simplify:

if condition {
    break
} else {
    doSomething()
}

To:

condition ? break : doSomething()

Note: the break statement can also be put in the second argument of the ternary operator too, this so the code would break if the condition was not met. A loop's label may be specified after the break keyword as well.

Usage Example:

The following function, getPrimes(n:), returns the first n prime numbers as an array of Ints.

func getPrimes(n: Int) -> [Int] {
    var primes = [Int]()
    var count = 1
    
    for x in 2... where !primes.contains { x % $0 == 0 } {
        primes.append(x)
        if count > n {
            break
        } else {
            count += 1
        }
    }
    return primes
}

print(getPrimes(n: 10))
// Prints "[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]"

Now, let's fix this code using our new and refined syntax:

func getPrimesAgain(n: Int) -> [Int] {
    var primes = [Int]()
    var count = 1
    for x in 2... where !primes.contains { x % $0 == 0 } {
        primes.append(x)
        count < n ? (count += 1) : break //syntactical change
        //We can alternatively use:
        //count += count < n ? 1 : break
    }
    return primes
}

print(getPrimesAgain(n: 10))
// Prints "[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]"

fallthrough


This will bring the use of the keyword fallthrough to the ternary operator.

This will simplify:

if condition {
    fallthrough
} else {
    doSomething()
}

To:

condition ? fallthrough : doSomething()

Note: the fallthrough statement can also be put in the second argument of the ternary operator too, this so the code would fallthrough if the condition was not met.

Usage Example:

First let's start by creating our Fruit enumeration.

enum Fruit {
    case apple, pear, orange, banana, grapes
} 

Now, let's create a function that takes a list (Array) of the fruits we have in our fruit basket. We want this function to tells us if we have an apple in our basket, but otherwise, tell us that we just have some random fruits.

func fruitBasket(with fruits: [Fruit]) {
    
    guard let firstFruit = fruits.first else { return }
    var str = "We have "
    switch fruits {
        
    case let basket where basket.contains(.apple):
        str.append("an apple and ")
        if fruits.count > 1 {
            fallthrough
        } else {
            str.removeLast(4)
        }
    default:
        str.append("some other fruits")
    }
    print(str, "in our basket")
}

let fruits : [Fruit] = [.apple, .pear, .banana]
fruitBasket(with: fruits)
// Prints "We have an apple and some other fruits in our basket"

let applelessFruits : [Fruit] =  [.pear, .orange, .grapes]
fruitBasket(with: applelessFruits)
// Prints "We have some other fruits in our basket"

Now, let's rejig our code above with our new and refined syntax:

func fruitBasketRefined(with fruits: [Fruit]) {

    guard let firstFruit = fruits.first else { return }
    var str = "We have "
    switch fruits {

    case let basket where basket.contains(.apple):
        str.append("an apple and ")
        fruits.count == 1 ? str.removeLast(4) : fallthrough //syntactical change
    default:
        str.append("some other fruits")
    }
    print(str, "in our basket")
}

let newFruits : [Fruit] = [.orange, .apple, .banana, .grapes]
fruitBasketRefined(with: fruits)
// Prints "We have an apple and some other fruits in our basket"

let newApplelessFruits : [Fruit] =  [.pear, .orange, .grapes]
fruitBasketRefined(with: newApplelessFruits)
// Prints "We have some other fruits in our basket"

return


This will bring the use of the keyword return to the ternary operator.

This will simplify:

if condition {
    return someValue
} else {
    doSomething()
}

To:

condition ? return someValue : doSomething()

Note: the return statement and someValue can also be put in the second argument of the ternary operator too, this so the code would return someValue if the condition was not met.

Usage Example:

We want to make a function pythagoreanTripleGCF(a:b:c:) where a, b, and c are values of type Int. If a, b, and c form a pythagorean triple, our function will return the greatest common factor of , , and . But if a, b, and c do not form a pythagorean triple, our function will return -1.

func pythagoreanTripleGCF(a: Int, b: Int, c: Int) -> Int {
    
    let (x, y, z) = (a * a, b * b, c * c)
    var n = -1
    
    if x + y == z {
        n = min(x, y, z)
    } else {
        return n
    }
    n = (1...n).reversed().first { x % $0 == 0 && y % $0 == 0 && z % $0 == 0 }!
    
    return n
}

let a = 6
let b = 8
let c = 10
// 6² + 8² = 10², ∴ a, b, and c are a valid pythagorean triple

print(pythagoreanTripleGCF(a: a, b: b, c: c)) //The GCF of 36, 64, and 100 is 4
// Prints "4"

Now, let's update our code above with our new and improved syntax.

func betterPythagoreanTripleGCF(a: Int, b: Int, c: Int) -> Int {
    
    let (x, y, z) = (a * a, b * b, c * c)
    var n = -1
    
    x + y == z ? (n = min(x, y, z)) : return n  //syntactical change
    
    n = (1...n).reversed().first { x % $0 == 0 && y % $0 == 0 && z % $0 == 0 }! 
    
    return n
}

let a = 6
let b = 8
let c = 10
// 6² + 8² = 10², ∴ a, b, and c are a valid pythagorean triple

print(betterPythagoreanTripleGCF(a: a, b: b, c: c)) //The GCF of 36, 64, and 100 is 4
// Prints "4"

continue


This will bring the use of the keyword continue to the ternary operator.

This will simplify:

if condition {
    continue
} else {
    doSomething()
}

To:

condition ? continue : doSomething()

Note: the continue statement can also be put in the second argument of the ternary operator too, this so the code would continue if the condition was not met. A loop's label may be specified after the continue keyword as well.

Usage Example:

Let's start off by making a function called transformedEvens(of:with:). The first parameter, arr, is of type [Int] and this will be the array that we are preforming the specified transformation on. The second parameter, transform, is of type (Int) -> Int and this specifies the transformation to perform on each even integer in the array (arr). After preforming the specified transformations on arr, we will return our newly transformed array newArr.

func transformedEvens(of arr: [Int], with transform: (Int) -> Int) -> [Int] {
    var newArr = [Int]()
  
    for var n in arr {
        if n % 2 == 0 {
            n = transform(n)
        } else {
            continue
        }
        newArr.append(n)
    }
    
    return newArr
}

let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let transformedArray = transformedEvens(of: array) { $0 * 10 }

print(transformedArray)
// Prints "[20, 40, 60, 80, 100]"

Now, let's take our transformedEvens function and freshen it up with our new syntactical changes.

func transformedEvensV2(of arr: [Int], with transform: (Int) -> Int) -> [Int] {
    var newArr = [Int]()
    for var n in arr {
        n % 2 == 0 ? (n = transform(n)) : continue //syntactical change
        newArr.append(n)
    }
    return  newArr
}

let newArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let newTransformedArray = transformedEvensV2(of: newArray) { $0 * 10 }

print(newTransformedArray)
// Prints "[20, 40, 60, 80, 100]"

Blank Argument `_`


This will bring the use of the keyword break to the ternary operator.

This will simplify:

if condition {
    doSomething()
} else {
    //empty
}

To:

condition ? doSomething() : _

Note: the blank `_` argument can also be put in the first argument of the ternary operator too, this so doSomething() would execute if the condition was not met. If indeed condition is false in the above line of code, the line will not be evaluated.

Usage Example:

Let's make a function called addEvens(from:to:). This function will take an array of Ints, arr, and return the value of each of its even integers added to n.

func addEvens(from arr: [Int], to n: Int) -> Int {
    var num = n
    for i in arr {
        if i % 2 == 0 {
            num += i
        }
    }
    return num
}

let number = 15
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

print(addEvens(from: array, to: number))
// Prints "45"

Now, let's redo our code above with our new ternary syntax:

func addEvensFixed(from arr: [Int], to n: Int) -> Int {
    var num = n
    for i in arr {
        i % 2 == 0 ? (num += i) : _ //syntactical change
        //We can alternatively use:
        //num += i % 2 == 0 ? i : _
    }
    return num
}


Ternary Operators that Return Values

Given the following code

var totalUnderTen = 0
for n in 1... {
    totalUnderTen += n < 10 ? n : break
}
// totalUnderTen = 45 = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9

The code will be accepted as valid syntax and will break if the condition is true, rather than returning the value of break so to say.

The same is true for all of the other aforementioned additions (break, fallthrough, _, return, continue).


Motivation

The primary motivation for this addition to the ternary operator is ease of writing quick code. This will help extend the functionality of the loved ternary operator, especially when dealing with control flow.


Thanks for the thorough pitch, with lots of good examples that made your idea clear. I think we have to question whether

count < n - 1 ? (count += 1) : break

is significantly better than

if count < n - 1 { count += 1 } else { break }

for people who want to write their control flow all on a single line. This is especially true for the _ case, where

i % 2 == 0 ? (num += i) : _

can already be written as

if i % 2 == 0 { num += i }

So my preliminary thought is that this doesn't seem like much of a brevity improvement over the status quo, and has the potential to obscure control flow. It also makes understanding code involving the ternary operator more difficult, since you can no longer just collapse it in your head to just being a value of some type.

4 Likes

For the first scenario:

I just updated my proposal to offer an alternative for that very part.

Another way to write it with my most recent change (setting count initially to 1 instead of 0):

count += count < n ? 1 : break

Would you not say that is better than:

if count < n { count += 1 } else { break }

I would say that the provided ternary syntax is better than the if else, but that's just my opinion. But regardless of the fact, shouldn't the option be available for people to use? I think so.

No, unfortunately, I would not say that the former is better. In fact, to me it's quite unreadable, whereas the existing spelling is instantly clear to the reader. Since code is read more often than it is written, I would conclude that enabling the first syntax is something that should be actively avoided.

Swift syntax is already criticized as having too many options; as an opinionated language, adding additional syntax is to be avoided if the only result is to provide more options rather than better ones.

Good effort on the pitch though; I think, overall, where you're trying to go is to make more control flow statements into expressions. I think switch is the highest yield in that regard, but in general the idea has some legs. I'm not sure that the example above reflects well on the overall idea, though.

2 Likes

This seems confusing to me. I understand it, but on first glance, what is a break statement doing inside of an assignment expression?

2 Likes

This will quickly lead to an unreadable mess.

a == 0 ? break : a == 1 ? return : a == 2 ? 3 : 4

Granted, nested ternary operators are difficult at the best of times, but I don't think being able to sprinkle different control flow statements is going to help.

1 Like

I'd have to say no to this idea. In fact I hate it. This idea turns the ternary operator into a crippled if statement.

The ternary operator is designed to be used in expressions. It has arguments that are expressions and it returns a value with a type. It is not a control flow statement. For this, we have the if ... else statement and this idea offers nothing over that except fewer keystrokes. The examples are not simpler than the code they replace, just shorter and (in my opinion) less readable.

5 Likes

Strongly agree with @jeremyp on this. It doesn't make the language any more expressive, encourages using ternaries as control flow rather than purely as expressions, and find the code harder to read.