[Pitch] for-where-as

Currently when iterating through a sequence of mixed types, Swift supports the for-where-is syntax, which lets you constrain a sequence to specific types.

Unfortunately this syntax requires additional type-casting that is unnecessary...

let items : [Any] = ["Hello", "World", 123]
                
for obj in items where obj is String {
    // obj is of Any type
   let str = obj as! String
}

In situations like this, "for-where-is" gives us little benefit over the "for-where-guard-continue" pattern:

let items : [Any] = ["Hello", "World", 123]
                
for obj in items {
    guard let str = obj as? String else { continue } 
}

As an alternative, what do people think of supporting a "for-where-as" pattern as a shorthand for these cases:

let items : [Any] = ["Hello", "World", 123]
                
for str in items where str as String {
    // str is of String
}

This can help especially in the case of type-erasure situations e.g. something like...

protocol AnyPlant {
  var name: String { get }
}

protocol Fruit: AnyPlant {
    var numberOfSeeds: { get } // because only fruits have seeds
}

protocol Vegetable: AnyPlant {
   var isRoot: Bool { get }
   var isStem: Bool { get }
   var isLeaf: Bool { get }
}

var myPlants: [ Fruit("apple"), Fruit("pear"), Vegetable("beet") ]

for fruit in myPlants as Fruit {
  print("\(fruit.name) is a fruit I love!")
}

Expected output:

apple is a fruit I love!
pear is a fruit I love!

3 Likes

Interesting idea. Could also use compactMap as an existing solution.

var myPlants: [ Fruit("apple"), Fruit("pear"), Vegetable("beet") ]

for fruit in myPlants.compactMap({ $0 as? Fruit }) {
    print("\(fruit.name) is a fruit I love!")
}
1 Like

You can use pattern matching to do this, if I understand correctly (playground on SwiftFiddle):

let items : [Any] = ["Hello", "World", 123]

// prints "Hello", "World" 
for case let string as String in items {
  // string is of String type
   print(string)
}

is that what you're looking for here?


(I wasn't able to free-hand this from memory, lol! I had to look up the syntax on stack overflow).

17 Likes

Hah, I also thought there was an existing solution for this, but couldn't manage to free-hand it. :sweat_smile:

Good work digging up the solution.

1 Like

@cal Yes, that's what I was looking for. Thanks! I still think the "for-where-as" is easier to use. Personally I get very confused about how to use "case" all its various forms.

5 Likes

Food for thought:

for obj in items.lazy.compactMap({ $0 as? String }) {
  ...
}

Or:

items.lazy.compactMap { $0 as? String }.forEach { obj in
  ...
}
2 Likes

To make it clear this is a conditional cast, I would rather use as?

for fruit in myPlants as? Fruit {
  print("\(fruit.name) is a fruit I love!")
}

Would it make more sense to place the as on the iteration element?

for fruit as? Fruit in myPlants {
    print("\(fruit.name) is a fruit I love!")
}
3 Likes

To make it clear this is a conditional cast, I would rather use as?

I disagree. as? suggest there are optionals involved which is not the case here.

protocol P {}

struct A: P {}
struct B: P {}

let p: P = A()

if let a = p as? A {
    print(a)
}

You’re dealing with two features here: a conditional cast that returns an optional, and an if-let statement that expects an optional. So you are working with an optional.

You can rewrite that to just do a single condition cast with no optionals:

if case let a as A = p {
    print(a)
}

(Not saying this is a better way to do it, just showing that the method not involving an optional doesn’t use as?)

as instead of as? is more consistent with other condition casting that doesn’t involve optionals, eg.

switch p {
case let a as A:
    print(a)
}

and

2 Likes

My point is that using as? instead of as is actually required in situations that are similar to the proposed for fruit in myPlants as? Fruit { syntax. Leaving off ? would likely imply that the cast cannot fail, and is checked at compile time, instead of at runtime. It is not similar to pattern matching syntax, because the implication is always that the match may fail.

Fair point. Still feels a bit inconsistent to me but it makes sense. I would be happy with either way.