Mocha
Mocha

Reputation: 2253

How to unwrap double optionals in switch statement -- Swift

xCode 10.2.1, Swift language unspecified (4, 4.2, 5)

I have two double optional bools. I would like to unwrap them in the switch statement as so..

func evaluate() -> Bool? {
    let lhs = try? getLhs() // lhs = Bool??; getLhs() returns Bool?
    let rhs = try? getRhs() // rhs = Bool??; getRhs() returns Bool?
    switch (lhs ?? nil, rhs ?? nil) {
    case (nil, nil):
        return nil
    case (nil, rhs):
        return rhs ?? nil
    case (lhs, nil):
        return lhs ?? nil
    default:
        guard case let lhs?? = lhs, case let rhs?? = rhs  else { return nil }
        return lhs || rhs
    }
}

This works, but I'm curious why I need to double unwrap the default case? I was thinking once the lhs and rhs get unwrapped from the switch inputs, the default case would work with a Bool? for lhs and rhs.

Update --

Thanks for answering. Yes, the double optional is overkill, but what I really wanted to understand was why my default was requiring me to unwrap again. I did not completely understand how switch works with optionals. Alexander helped me understand all the iterations and vacawama gave me a clean implementation.

Upvotes: 0

Views: 1177

Answers (2)

vacawama
vacawama

Reputation: 154583

This works, but I'm curious why I need to double unwrap the default case? I was thinking once the lhs and rhs get unwrapped from the switch inputs, the default case would work with a Bool? for lhs and rhs.

You aren't changing lhs and rhs when you do lhs ?? nil and rhs ?? nil. You are creating new values. So when you get to the default case, lhs and rhs are still Bool??. You can use let lhs and let rhs to capture the unwrapped values as I did in my solution below.


Here is another way to do it. A bit cleaner with some pattern matching:

switch (lhs as? Bool, rhs as? Bool) {
case (nil, nil):
    return nil
case (nil, let rhs):
    return rhs
case (let lhs, nil):
    return lhs
case (let lhs?, let rhs?):
    return lhs || rhs
}

Explanation

Casting the Bool?? with as? Bool leaves you with a Bool?. The let rhs and let lhs in the pattern matching catch the Bool? value so that it can be returned. In the final case, let lhs? and let rhs? unwrap the Bool? values to get Bool values so that || can be performed.


Test Cases

test(nil, nil) // nil
test(nil, false) // false
test(nil, true) // true
test(nil, Optional(nil)) // nil
test(nil, Optional(false)) // false
test(nil, Optional(true)) // true

test(false, nil) // false
test(false, false) // false
test(false, true) // true
test(false, Optional(nil)) // false
test(false, Optional(false)) // false
test(false, Optional(true)) // true

test(true, nil) // true
test(true, false) // true
test(true, true) // true
test(true, Optional(nil)) // true
test(true, Optional(false)) // true
test(true, Optional(true)) // true

test(Optional(nil), nil) // nil
test(Optional(nil), false) // false
test(Optional(nil), true) // true
test(Optional(nil), Optional(nil)) // nil
test(Optional(nil), Optional(false)) // false
test(Optional(nil), Optional(true)) // true

test(Optional(false), nil) // false
test(Optional(false), false) // false
test(Optional(false), true) // true
test(Optional(false), Optional(nil)) // false
test(Optional(false), Optional(false)) // false
test(Optional(false), Optional(true)) // true

test(Optional(true), nil) // true
test(Optional(true), false) // true
test(Optional(true), true) // true
test(Optional(true), Optional(nil)) // true
test(Optional(true), Optional(false)) // true
test(Optional(true), Optional(true)) // true

Upvotes: 2

Alexander
Alexander

Reputation: 63167

  • Bool has 2 values:
    • true
    • false
  • Bool? has 3 values, .none (a.k.a. nil), and one .some case wrapping each of the 2 possible cases of Bool:
    • .some(true)
    • .some(false)
    • .none
  • Bool?? has 4 values, .none, and one .some case wrapping each of the 3 possible cases of Bool?:
    • .some(.some(true))
    • .some(.some(false))
    • .some(.none)
    • .none

To make a first approximation of a solution, we can enumerate all possible values:

func getLhs() throws -> Bool? { return nil }
func getRhs() throws -> Bool? { return nil }

func f() -> Bool? {

    let lhs: Bool?? = try? getLhs()
    let rhs: Bool?? = try? getRhs()
    switch (lhs, rhs) {
        case (.some(.some(true)), .some(.some(true))): return true
        case (.some(.some(true)), .some(.some(false))): return true
        case (.some(.some(true)), .some(.none)): return true
        case (.some(.some(true)), .none): return true

        case (.some(.some(false)), .some(.some(true))): return true // result of OR
        case (.some(.some(false)), .some(.some(false))): return false
        case (.some(.some(false)), .some(.none)): return false
        case (.some(.some(false)), .none): return false

        case (.some(.none), .some(.some(true))): return true
        case (.some(.none), .some(.some(false))): return false
        case (.some(.none), .some(.none)): return nil
        case (.some(.none), .none): return nil

        case (.none, .some(.some(true))): return true
        case (.none, .some(.some(false))): return false
        case (.none, .some(.none)): return nil
        case (.none, .none): return nil
    }
}

So now this is obviously crazy, but we can collapse cases by binding variables:

switch (lhs, rhs) {
    case let (.some(.some(l)), .some(.some(r))): return l || r
    case let (.some(.some(l)), .some(.some(r))): return l || r
    case let (.some(.some(l)), .some(.none)): return l
    case let (.some(.some(l)), .none): return l

    case let (.some(.some(l)), .some(.some(r))): return l || r
    case let (.some(.some(l)), .some(.some(r))): return l || r
    case let (.some(.some(l)), .some(.none)): return l
    case let (.some(.some(l)), .none): return l

    case let (.some(.none), .some(.some(r))): return r
    case let (.some(.none), .some(.some(r))): return r
    case (.some(.none), .some(.none)): return nil
    case (.some(.none), .none): return nil

    case let (.none, .some(.some(r))): return r
    case let (.none, .some(.some(r))): return r
    case (.none, .some(.none)): return nil
    case (.none, .none): return nil
}

And we can remove the duplicate cases:

switch (lhs, rhs) {
    case let (.some(.some(l)), .some(.some(r))): return l || r
    case let (.some(.some(l)), .some(.none)): return l
    case let (.some(.some(l)), .none): return l

    case let (.some(.none), .some(.some(r))): return r
    case (.some(.none), .some(.none)): return nil
    case (.some(.none), .none): return nil

    case let (.none, .some(.some(r))): return r
    case (.none, .some(.none)): return nil
    case (.none, .none): return nil
}

We can then group all the nested nil cases:

switch (lhs, rhs) {
    case let (.some(.some(l)), .some(.some(r))): return l || r

    case let (.some(.some(l)), .some(.none)): return l
    case let (.some(.some(l)), .none): return l

    case let (.some(.none), .some(.some(r))): return r
    case let (.none, .some(.some(r))): return r

    case (.some(.none), .some(.none)),
        (.some(.none), .none),
        (.none, .some(.none)),
        (.none, .none):
        return nil
}

We can then just catch all those in a default:

switch (lhs, rhs) {
    case let (.some(.some(l)), .some(.some(r))): return l || r

    case let (.some(.some(l)), .some(.none)): return l
    case let (.some(.some(l)), .none): return l

    case let (.some(.none), .some(.some(r))): return r
    case let (.none, .some(.some(r))): return r

    default: return nil
}

But this whole thing is crazy

Why do you have a function that throws AND returns an Optional? It would be much better to make the function return a non optional bool, and have the nil cases handled by one of the exceptions. Alternatively, you could return a Result<Bool, Error>, where one of the error cases encoded the nil.

If you had to stick with these types, a switch is the wrong way to go about it entirely. You could use Optional.map or Optional.flatMap, but those would get hairy too.

Upvotes: 2

Related Questions