Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to write an `if case` statement as an expression?

Tags:

swift

Consider this code:

enum Type {
    case Foo(Int)
    case Bar(Int)

    var isBar: Bool {
        if case .Bar = self {
            return true
        } else {
            return false
        }
    }
}

That's gross. I would like to write something like this instead:

enum Type {
    case Foo(Int)
    case Bar(Int)

    var isBar: Bool {
        return case .Bar = self
    }
}

But such a construct does not seem to exist in Swift, or I cannot find it.

Since there's data associated with each case, I don't think it's possible to implement the ~= operator (or any other helper) in a way that's equivalent to the above expression. And in any case, if case statements exist for free for all enums, and don't need to be manually implemented.

Thus my question: is there any more concise/declarative/clean/idiomatic way to implement isBar than what I have above? Or, more directly, is there any way to express if case statements as Swift expressions?

like image 377
Ian Henry Avatar asked Mar 25 '16 15:03

Ian Henry


2 Answers

So, there is a neater way, but requires a 3rd-party package: CasePaths

The idea is they work similarly to KeyPaths, and they come with a / operator to trigger it. There is also a ~= operator to check if a CasePath matches an instance.

So, you can achieve something like your original example like so:

import CasePaths

enum Type {
    case Foo(Int)
    case Bar(Int)

    var isBar: Bool {
        /Self.Bar ~= self
    }
}

You can also get the value:

extension Type {
    /// Returns the `Int` if this is a `Bar`, otherwise `nil`.
    var barValue: Int? {
        (/Self.Bar).extract(from: self)
    }
}

You can do several other useful things with CasePaths as well, such as extracting the Foo values in an array of Type values:

let values: [Type] = [.Foo(1), .Bar(2), .Foo(3), .Foo(4), .Bar(5)]
let foos = values.compactMap(/Type.Foo) // [1, 3, 4]
let bars = values.compactMap(/Type.Bar) // [2, 5]

I'm sure there is somewhat of a performance cost, but it may not be an issue in your context.

like image 180
David Peterson Avatar answered Mar 15 '23 05:03

David Peterson


UPDATE 2: Another workaround... Create a var that returns an Int ONLY based on the case, then use a static (or instance, I thought static looked cleaner) method to test equivalence of just the case. It won't clash with Equatable, you don't have to overload an operator (unless you want to replace the static method with one), and you also wouldn't have to create separate var isFoo, var isBar, etc.

I know you used this example to ask a more generic question (how can I use 'if case' as an expression?) but if that's not possible, this may be a valid workaround. I apologize if this treats "the symptoms" not "the problem"

enum Something{
    case Foo(Int)
    case Bar(Int)

    static func sameCase(a: Something, b: Something) -> Bool {
        return a.caseValue == b.caseValue
    }

    var caseValue: Int {
        switch self {
        case .Foo(_):
            return 0
        case .Bar(_):
            return 1
        }
    }

    //if necessary
    var isBar: Bool {
        return Something.sameCase(self, b: Something.Bar(0))
    }
}

Something.sameCase(.Bar(0), b: .Foo(0)) // false
Something.sameCase(.Bar(1), b: .Foo(2)) // false
Something.sameCase(.Foo(0), b: .Foo(0)) // true
Something.sameCase(.Bar(1), b: .Bar(2)) // true


Something.Bar(0).isBar // true
Something.Bar(5).isBar // true
Something.Foo(5).isBar // false

UPDATE 1:

Ok, so this seems to work. If you overload the == operator to ignore values and return true only when both enums are the same case, you can pass any value in your isFoo method and still determine the type.

I'm assuming you will need to customize this function to accommodate the the associated values, but it seems like a step in the right direction

enum Something {
    case Foo(Int)
    case Bar(Int)

    var isFoo: Bool {
        return self == .Foo(0) // number doesn't matter here... see below
    }
}

func ==(a: Something, b: Something) -> Bool {
    switch (a,b) {
    case (.Bar(_), .Bar(_)):
        return true
    case (.Foo(_), .Foo(_)):
        return true
    default:
        return false

    }
}

let oneFoo = Something.Foo(1)
let twoFoo = Something.Foo(2)
let oneBar = Something.Bar(1)
let twoBar = Something.Bar(2)

oneFoo == twoFoo // true
oneFoo == oneFoo // true
oneFoo == oneBar // false
oneFoo == twoBar // false

OLD:

You can use self and the case name to directly check which case it is, you don't have to use the case keyword. Hopefully this will work for your situation:

enum Something{
    case Foo(Int)
    case Bar(Int)

    var isFoo: Bool {
        switch self {
        case Foo:
            return true
        case Bar:
            return false
        }
    }
}
like image 36
Doug Mead Avatar answered Mar 15 '23 06:03

Doug Mead