arraysswiftthrowlazy-sequences

Lazily Mapping an Array Using a Throwing Closure


When lazily mapping an array of values, I receive an instance of type LazyMapSequence as expected:

Welcome to Apple Swift version 5.7 (swiftlang-5.7.0.127.4 clang-1400.0.29.50).
Type :help for assistance.
  1> let numbers = Array(1...5)
numbers: [Int] = 5 values {
  [0] = 1
  [1] = 2
  [2] = 3
  [3] = 4
  [4] = 5
}
  2> let squares = numbers.lazy.map { $0 * $0 }
squares: LazyMapSequence<LazySequence<[Int]>.Elements, Int> = {
  _base = 5 values {
    [0] = 1
    [1] = 2
    [2] = 3
    [3] = 4
    [4] = 5
  }
  _transform =
}

However, if the map(_:) method receives a throwing a closure instead, the mapping is not performed lazily, and I receive an array instead:

  3> func square(_ x: Int) throws -> Int {
  4.     return x * x
  5. }
  6> let squares = try numbers.lazy.map(square)
squares: [Int] = 5 values {
  [0] = 1
  [1] = 4
  [2] = 9
  [3] = 16
  [4] = 25
}

Why is that, and how do I lazily map an array of values using a throwing closure?


Solution

  • A workaround is

    extension LazySequennce {
        func tryMap<U>(_ transform: @escaping (Self.Element) throws -> U) -> LazyMapSequence<Self.Elements, Result<U, Error>> {
            self.map { x in Result(catching: { try transform(x) }) }
        }
    }
    

    Note that the element type of the sequence is Result<U, Error>. We essentially "catch" the error whenever any is thrown. The error has to be caught because when iterating over any Sequence, the protocol requires that no errors are thrown.

    As for why map(square) is not lazy, it is exactly as you have observed. LazySequenceProtocol.map takes a closure that does not throw.

    func map<U>(_ transform: @escaping (Self.Element) -> U) 
        -> LazyMapSequence<Self.Elements, U>
    

    When you pass in the throwing method, it instead calls Sequence.map:

    func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]
    

    which is not lazy.

    This would all be solved if there were a method that looked like:

    func tryMap<U>(_ transform: @escaping (Self.Element) throws -> U) 
        -> LazyThrowingMapSequence<Self.Elements, U>
    

    However, such a LazyThrowingMapSequence type cannot conform to Sequence, because its iterator cannot conform to IteratorProtocol. Its iterator's next method throws, but IteratorProtocol requires that next does not throw.

    It is theoretically possible to write LazyThrowingMapSequence by just adding throws to a few places to LazyMapSequence. (Source code of LazyMapSequence is here) But using it would be a pain, since you cannot iterate over it with a for loop, and it doesn't have any of those convenient methods from the Sequence protocol.