Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A single expression for guard case let with type casting of the associated type?

Tags:

swift

swift3

I have the following enum:

enum JSONData {
    case dict([String:Any])
    case array([Any])
}

I would like to do a pattern matching assignment with type casting using guard case let. I not only want to make sure that someData is .array, but that its associated type is [Int] and not just [Any]. Is this possible with a single expression? Something like the following:

let someData: JSONData = someJSONData()
guard case let .array(anArray as [Int]) = someData
else { return }

But the above does not compile; the error is downcast pattern value of type '[Int]' cannot be used. I know this is possible with the following but I'd rather do it in a single expression if possible.

guard case let .array(_anArray) = someData, let anArray = _anArray as? [Int]
else { return }

Edit on April 27, 2018: An update to this situation: you may now use the following syntax as long as the cast type is not a Collection:

enum JSONData {
    case dict([String:Any])
    case array(Any)  // Any, not [Any]
}

func f() {
    let intArray: JSONData = .array(1)
    guard case .array(let a as Int) = intArray else {
        return
    }
    print("a is \(a)")
}

f()  // "a is 1"

If you attempt to use a collection type such as [Int], you receive the error error: collection downcast in cast pattern is not implemented; use an explicit downcast to '[Int]' instead.

like image 611
BallpointBen Avatar asked Jul 16 '17 01:07

BallpointBen


1 Answers

What you're trying to do is not possible due to a limitation in Swift's pattern matching implementation. It's actually called out here:

// FIXME: We don't currently allow subpatterns for "isa" patterns that
// require interesting conditional downcasts.

This answer attempts to explain why, though falls short. However, they do point to an interesting piece of information that makes the things work if you don't use a generic as the associated value.

The following code achieves the one-liner, but loses some other functionality:

enum JSONData {
    case dict(Any)
    case array(Any)
}

let someData: JSONData = JSONData.array([1])

func test() {
    guard case .array(let anArray as [Int]) = someData else {
        return
    }

    print(anArray)
}

Alternatively, the same one liner can be achieved through a utility function in the enum definition that casts the underlying value to Any. This route preserves the nice relationship between the cases and the types of their associated values.

enum JSONData {
    case dict([String : Any])
    case array(Array<Any>)

    func value() -> Any {
        switch self {
        case .dict(let x):
            return x
        case .array(let x):
            return x
        }
    }
}

// This coercion only works if the case is .array with a type of Int
guard let array = someData.value() as? [Int] else {
    return false
}
like image 112
allenh Avatar answered Oct 21 '22 06:10

allenh