[Pitch] Extending optional chains to include for loops

It should only be necessary for the last step, no? for x in some?.optional?.chain ?? .empty? Or are you objecting to having to put .empty on a substantial proportion of your for loops?

1 Like

Yes (… ?? .empty). I am happy if somewhere a question mark makes it clear that we are dealing with an optional sequence (and I am not the one who has to change the compiler :wink:). Do not want to add 30 times … ?? .empty in a source file. Well, this is a personal opinion of course. Other people think the question mark is not enough…

1 Like

I updated the pitch draft:

  • Added the bullet points by @Ben_Cohen above (regarding for x in y? , for x in y.orEmpty).
  • Renamed alternative 4 to ´orEmpty`.
  • Added ... ?? .empty as a variation of alternative 3.
  • Used the fact that forEach might be considered bad practice as an argument in favour of the proposed change. (And no more talk about a "symmetry" of forEach vs. for-in loops.)

Well, the pitch draft evolved over time and it is kind of difficult for me to judge the quality of it. As I already said, a fresh start might be helpful. But maybe it is OK as it is, I at least hope that all arguments and alternatives are mentioned.

The downside of such a pitch is of course that it is already written in favour of a certain solution. So even if alternatives and arguments against the proposed solution are mentioned, it is not a "neutral" text. So I am even not sure if a pitch is the right thing to do at this point.

Evolution proposals have an Alternatives Considered section, not Arguments Against. They have no obligation to be neutral. Other people can argue against your proposal in its pitch and review; maybe you can change their minds with a different argument, or maybe you just disagree about some judgment inherent in the proposal. Ultimately, you want the proposal document to make as strong a case as it can, but don’t get too disheartened if there are people who still aren’t convinced.

6 Likes

We shouldn't turn this into a forEach debate, since it's related but tangential. However,

Yes indeed.

Not so. Use of forEach is trivially replaceable with a for loop in all cases, other than the one being debated here.

Not really. I would say more that forEach should be ignored for our purposes. It's neither a reason for or against the two options discussed here.

Just to be clear: I'm not sure which of ? vs .orEmpty is better. Probably either would be just fine, since they both have advantages over the other and no major downsides. I just think both need to have the right amount of discussion to ensure the best choice is made.

I understand why one would not like to mix up those two issues too much, but: If forEach is considered bad practice, then for-in loops should be a good replacement for the cases where people still feel the need to use forEach, i.e. for-in loops should work fine in "all" cases without much additional burden on the syntax, but they do not. I would like to use for-in loops only, but not at their current state.

There are objections against changing for-in loops at all concerning optional chaining or optional sequences because of “non-obvious ways” of introducing non-optional sequences. .orEmpty even drops the question mark so I think this is even more “non-obvious”. I consider this a major downside.

1 Like

I believe this would be an inadvisable use of the ?? operator (which is already challenging from a type checking perspective, which I think that would rule this option out even if it was a good idea, but that's secondary to it not being a good idea).

The ?? operator currently means "here is an optional value of a wrapped type. If the optional on the LHS is nil, substitute this other value, giving you a value of the wrapped type."[1] So the type signature of ?? is (Wrapped?,Wrapped)->Wrapped.

But this is not what we want here. We do not want to produce the wrapped value. We cannot, in the general case, do this for collections we cannot create an empty instance of (like CollectionOfOne or an opaque some Sequence).

Instead what you're looking for is an operator that says "given an optional value, return a type that's either the wrapped type if it wasn't nil, or some other type if it was". So your ?? would be (Wrapped,OrElse)->Either<Wrapped,OrElse>.

This Either type is extremely useful, and totally implementable and I do think we should add it to the standard library. It's super useful because you can conform it to lots of protocols when both the types conform – like Sequence. But if we do, we should not overload ?? to turn optionals into it, and I also think we should still give a more bespoke affordance for iterating optional collections easily that doesn't rely on it, since it's a pretty advanced feature that too much of progressive disclosure to understand.

(Of course, a sequence-conforming Either could be the type returned by Sequence.orEmpty if that's the route we go down)

More on Either if you're interested

The benefit of using Either to solve this problem is that it allows more than just empty sequences...

// let's say ?| turns an Optional into an Either
infix operator ?|

func ?| <S: Sequence>(lhs: S?, rhs: [S.Element]) -> Either<S,[S.Element]> {
    if let lhs { .left(lhs) } else { .right(rhs) }
}

let maybeArray: [Int]? = nil 

for x in maybeArray ?| [99] {
    print(x) // prints 99
}

You might see though that I used a concrete array type for the rhs of ?|, rather than the more appealing solution of an arbitrary generic sequence, which I then happen to use Array with in the for loop. This is because I don't think you can make Either conform to ExpressibleByArrayLiteral because AFAICT the way that protocol works it can't be forwarded to the wrapped type.

EDIT: I was being way to over specific with the implementation of ?|, this just works:

func ?| <S,T>(lhs: S?, rhs: T) -> Either<S,T> {
    if let lhs { .left(lhs) } else { .right(rhs) }
}

Implementation of Either that makes the above snippet work:

enum Either<Left, Right> {
  case left(Left), right(Right)
}

typealias EitherSequence<L: Sequence, R: Sequence> =
  Either<L,R> where L.Element == R.Element

extension EitherSequence {
  internal struct Iterator {
    var left: Left.Iterator?
    var right: Right.Iterator?
  }
}

extension Either.Iterator: IteratorProtocol {
  internal typealias Element = Left.Element

  internal mutating func next() -> Element? {
    left?.next() ?? right?.next()
  }
}

extension EitherSequence: Sequence {
  internal typealias Element = Left.Element

  internal func makeIterator() -> Iterator {
    switch self {
    case let .left(l):
      Iterator(left: l.makeIterator(), right: nil)
    case let .right(r):
      Iterator(left: nil, right: r.makeIterator())
    }
  }
}

This is needlessly duplicating [] and [:], which already exist for this purpose.


  1. yes I know there's a second overload that takes an optional type on the rhs, too... this is part of the reason it's hellish for type checking ↩︎

7 Likes

There isn't a hard rule that every operation involving optionals must include a question mark (map and flatMap exist, for instance). I don't think the claim that for x in a.orEmpty is unclear is particularly credible (especially given you can just option-click it to see what it does the first time you hit it).

for x in a? certainly has the advantage of being more succinct, and I agree the symmetry with other optional chaining is nice. I'm just saying, lack of clarity of the .orEmpty option doesn't stand up as an argument against, to me.

2 Likes

Not for exactly the same scenario but I’ve been using nonEmpty for a while and a similar solution to the orEmpty for the topic at hand.
I find those extensions very appealing and I use them all the time.
The advantage is that they can be just in user level code. Maybe worth adding them to some semi-official package so the community can form a better opinion before changing the language or stdlib just for opinionated sugar.

1 Like

These extensions can come in handy, but I would prefer if there is an idiomatic way. The reader of your code should immediately understand the pattern, and there should not be different, but similar or even conflicting implementations when you are mixing in several packages (cf. this other forums comment).

The fact that .empty can substitute for both [] and [:] in a generic context is what makes it useful and not duplicative. But as you and I have both noted, that’s also what makes it problematic, because CollectionOfOne.empty is nonsensical. Personally I think this implies that CollectionOfOne should not exist, but that’s irrelevant at this point.

1 Like

Got you. Could we do it differently without creating empty sequences?

Essentially to treat:

    for x in y ?? .empty {
    }

exactly the same as if it was written:

    if let y {
        for x in y {
        }
    }

Understandably this might not be possible without changing the compiler itself.

Personally I think [:] should not exist.

Details
var x: [Int] = []
var y: [Int: Int] = [] // type is inferred
var z: Set<Int> = []

foo(array: [Int], dictionary: [Int:Int], set: Set<Int>) {...}
foo(array: [], dictionary: [], set: []) // type is inferred

very rarely that would lead to an ambiguity:

foo(_ array: [Int]) {...}
foo(_ dictionary: [Int:Int]) {...}

foo([]) // 🤔

but that's something we already have:

foo(_ array: [Int]) {...}
foo(_ set: Set<Int>) {...}
foo([]) // 🤔

and know how to deal with:

foo([] as [Int])      // 😀
foo([] as [Int:Int])  // 😀
foo([] as Set<Int>)   // 😀

2 Likes

Fair enough, but @Ben_Cohen also pointed out a subtler issue: for works on all Sequences, not just Collections. [] is clearly an inappropriate way to create an empty sequence, and it seems far stranger to make .empty a protocol requirement of Sequence than of Collection.

Sure, if you're willing to teach the compiler to specially handle something, it can do all sorts. But I'm not seeing why this would be better to either of the other two options. It would just be some weird hard-coded special case that looks like a library implementable thing (deliberately echoes an existing one in fact), but isn't. At least for x in y?, while also needing a special case in the compiler, is very clearly that. Whereas if we want a library solution that can work today, for x in y.orEmpty seems fine too.

1 Like

I don't see the benefits this offers, but I want to mention regardless that you could do the following in your codebase:

extension Optional: Sequence where Wrapped: Sequence {
    public struct Iterator: IteratorProtocol {
        public typealias Element = Wrapped.Element

        var underlying: Wrapped.Iterator?

        public mutating func next() -> Wrapped.Element? {
            underlying?.next()
        }
    }

    public func makeIterator() -> Iterator {
        Iterator(underlying: self?.makeIterator())
    }
}

var names: [String]? = nil

for name in names {
    print("Never prints \(name)")
}

In my years of using Swift, I've seen a lot of developers that force unwrap their variables. It's a real big issue, largely powered by the fact that Xcode suggests it, and a lot of developers simply click the button that solves their compiler error the fastest. That ends up being force unwrap most of the time.

Fundamentally, having optionals is often a scenario of uncertainty. You don't know if you have the value until you check. And that's great if used well, but can be devastating for productivity in the wrong hands.

Adding this proposal would simplify the flow for this group of developers, because they don't 'have' to unwrap the property. But the fundamental question doesn't get asked: "Does this value exist?"


I really want to emphasise a seemingly unrelated bit of Swift here - enums. Enums are awesome, because if you don't use the default: statement (which I really like to prevent through a linter), you're (almost) guaranteed to have exhaustively thought about all your scenarios. What happens with your enum case becomes a conscious decision, regardless of what case it is.

By not having a default statement, and using a switch rather than if myCase == .something, I also enable the Swift compiler to help me in 2 ways.

  1. Swift completes the list of cases I need to think about in my switch statements, and this is even more helpful in more complex projects, where I'd use nested enums for state.
  2. Adding a case will immediately notify me of all the switch statements where my enum case (potentially) has effect. So by not having a default: case, I'm "forced" (and happily so) to consider the impact of my change exhaustively.

The above makes for extremely well-thought out code, often reduces the amount of bugs/unexpected scenarios, and in turn saves a lot of time.


Back to optionals, I feel that not having to unwrap optionals when you're actively intending to use them is the most efficient road to both seeding confusion and encourages users to keep their value in a state of uncertainty. I like my code/functions to be clear and easily predictable, and a large part of that is unwrapping optionals when I need them. In my guard-let statements, I make a conscious decision what I do if I miss information. And in my if-let statements I consciously open myself open to multiple paths -with or without the value.

I probably don't need to explain that I'm against this proposal, but hope this clarifies why.

I really don't see your reasoning against special loop syntax for optionals here. Your points about optionals, while true, don't seem especially relevant to the question at hand. Optional chaining has always answered the question of "Does this value exist?" with "I don't care, but if it does, do this." This proposal extends the same principle to for in. Do you not use optional chaining at all?

Actually I view that as an advantage. It would look exactly like a library feature (and the only thing making it different is that it can't be implemented as a pure library feature† that's why it is special-cased in the compiler), and users won't have to learn a new syntax. The "perceived" language size is not increased.

† or maybe it could?

// using MaybeSequence from https://forums.swift.org/t/pitch-extending-optional-chains-to-include-for-loops/65848/15

struct MaybeSequence<Wrapped: Sequence> {
    let wrapped: Wrapped?
}

extension MaybeSequence {
    struct Iterator {
        var wrapped: Wrapped.Iterator?
    }
}

extension MaybeSequence.Iterator: IteratorProtocol {
    mutating func next() -> Wrapped.Element? {
        wrapped?.next()
    }
}

extension MaybeSequence: Sequence {
    func makeIterator() -> Iterator {
        .init(wrapped: wrapped?.makeIterator())
    }
}

enum EmptySequence {
    case empty
}

func ?? <Wrapped> (lhs: Optional<Wrapped>, rhs: EmptySequence) -> MaybeSequence<Wrapped> {
    MaybeSequence(wrapped: lhs)
}

func test() {
    let y: [Int]? = [1, 2, 3]
    let z: [Int]? = nil
    
    for x in y ?? .empty {
        print(x)
    }
    for x in z ?? .empty {
        print(x)
    }
}

test()

I try to avoid long chains as much as possible. But having a long chain is much more explicit than what's proposed here. I feel that implying a no-op in a for-loop is a pretty confusing way to go about things, that likely distracts people from having to think about the scenario nil.