1

Is this a clean way to convert a list of T?'s to an optional list of T? (i.e., [T?] to [T]?) If a nil value is found, nil instead of T, otherwise output the normal list with no Optional.

For example:

let x = [1, 2, 3, 4]
x.someMethod()  // [1, 2, 3, 4]

let y = [1, 2, nil, 3, 4]
y.someMethod()  // nil
Monolith
  • 1,067
  • 1
  • 13
  • 29

4 Answers4

4

This is a fast solution:

extension Array {
    func liftNil<T>() -> [T]? where Element == Optional<T> {
        if (allSatisfy {$0 != nil}) {
            return map { $0! }
        } else {
            return nil
        }
    }
}

It checks if all the elements are not nil, then force-unwraps them if they aren't.

Usage:

let x: [Int?] = [1, 2, 3, 4]
print(x.liftNil())  // prints [1, 2, 3, 4]

let y = [1, 2, nil, 3, 4]
print(y.liftNil())  // prints nil

Benchmarks:

name          time           std        iterations
--------------------------------------------------

all satisfy 0      147.000 ns ± 2031.47 %    1000000
all satisfy 1   247828.000 ns ±  22.01 %        5328
all satisfy 2   124486.000 ns ±  39.23 %       10274
all satisfy 3  1065633.500 ns ±  21.17 %        1158

exceptions 0     51785.000 ns ± 439.14 %       24331
exceptions 1    798534.500 ns ±  19.08 %        1600
exceptions 2    423202.000 ns ±  68.97 %        2978
exceptions 3    807380.500 ns ±  31.74 %        1632

prefix 0           174.000 ns ± 665.61 %     1000000
prefix 1        276945.000 ns ±  16.10 %        4402
prefix 2        139071.000 ns ±  21.79 %        8968
prefix 3      14788294.000 ns ±  17.71 %         102

loop 0            2520.000 ns ± 1559.85 %     535007
loop 1         1645015.000 ns ±  80.56 %         823
loop 2          861541.000 ns ±  27.66 %        1298
loop 3         1594589.500 ns ±  17.97 %         798

cast 0           55364.000 ns ± 204.48 %       22089
cast 1        14288104.500 ns ±  16.70 %          92
cast 2         8464850.000 ns ± 216.15 %         188
cast 3        14432597.000 ns ±  32.76 %         100

compact map 0  1653247.500 ns ±  25.78 %         696
compact map 1  1618238.500 ns ±  25.45 %         740
compact map 2  1685990.000 ns ±  20.90 %         649
compact map 3  1604636.000 ns ±  20.45 %         805

Test cases:

// 0: nil at front
var arrTest0 = [Int?](repeating: 0, count: 5000)
arrTest1[0] = nil

// 1: nil at end
var arrTest1 = [Int?](repeating: 0, count: 5000)
arrTest2[4999] = nil

// 2: nil in middle
var arrTest2 = [Int?](repeating: 0, count: 5000)
arrTest3[2500] = nil

// 3: no nils at all
var arrTest3 = [Int?](repeating: 0, count: 5000)

As you can see, my solution using allSatisfy quite fast. Note that Martin R's "just for fun solution" using exceptions is very fast in the test case without nils. There seems to be no "clear winner", but only tradeoffs between performance in different cases.

I benchmarked everything using google's swift-benchmark. The benchmark code can be found on this github gist.

Also note, it would be worth benchmarking arrays with more than one nil.


This code isn't very elegant, but it's performant as it only really does (at most) 1 pass through the array. Is't also faster than Martin R's solution when there is a nil as it immediately returns:

As the benchmarks show, this is actualy slower than my original solution. I think it's because of the overhead of the .append method.

extension Array {
    func liftNil<T>() -> [T]? where Element == Optional<T> {
        var new: [T] = []
        new.reserveCapacity(count)
        for item in self {
            if let value = item {
                new.append(value)
            } else {
                return nil
            }
        }
        return new
    }
}
Monolith
  • 1,067
  • 1
  • 13
  • 29
  • Can you provide some benchmark in your answer? – Kamil.S Jul 29 '21 at 18:26
  • There is no need to iterate over all elements with `allSatisfy`. Just finding one nil element is enough with `contains` or `first(where:` to "disqualify" an array as your non-nil result under your criteria. – Kamil.S Jul 30 '21 at 17:10
  • @Kamil.S actually, there's no difference in performance because `allSatisfy` will short-circuit and return `false` when it sees one `false` value. Under the hood, `allSatisfy` [uses `contains`](https://github.com/apple/swift/blob/main/stdlib/public/core/SequenceAlgorithms.swift#L540). I opted to use `allSatisfy`, but `contains` could work too. – Monolith Jul 30 '21 at 17:49
4

You can simply conditionally cast [T?] to [T], this succeeds exactly if the array contains no nil elements:

let x: [Int?] = [1, 2, 3, 4]
print(x as? [Int]) // Optional([1, 2, 3, 4])

let y: [Int?] = [1, 2, nil, 3, 4]
print(y as? [Int]) // nil

As an extension method of Array that would be

extension Array {
    func someFunction<T>() -> [T]? where Element == Optional<T> {
        return self as? [T]
    }
}

Previous, more complicated solutions: You can use compactMap() to get the non-nil elements, and then check if that is the same number as the original number of elements:

extension Array {
    func someFunction<T>() -> [T]? where Element == Optional<T> {
        let nonNils = compactMap { $0 }
        return nonNils.count == count ? nonNils : nil
    }
    
}

(The approach from Swift: Extension on [<SomeType<T>?] to produce [<T>?] possible? is used here to define an extension method for arrays of optional elements.)

Examples:

let x: [Int?] = [1, 2, 3, 4]
print(x.someFunction()) // Optional([1, 2, 3, 4])

let y = [1, 2, nil, 3, 4]
print(y.someFunction()) // nil

An alternative, just for fun: If you define an “unwrap or throw” method for optionals

extension Optional {
    struct FoundNilError : Error { }
    
    func unwrapOrThrow() throws -> Wrapped {
        switch self {
        case .some(let wrapped) : return wrapped
        case .none: throw FoundNilError()
        }
    }
}

then you can use that with map and the “optional try”:

extension Array {
    func someFunction<T>() -> [T]? where Element == Optional<T> {
        try? map { try $0.unwrapOrThrow() }
    }
}

As soon as a nil element is found, map throws an error, and try? returns nil.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Funnily enough, my benchmarks show that the "just for fun" solution is actualy quite fast when there are no nils :) – Monolith Jul 29 '21 at 19:22
  • @Monolith: I have added another, very simple method, perhaps you can benchmark that as well. (It is so simple that one would not really need to implement it as a method.) – Martin R Jul 29 '21 at 19:32
  • Yes, I just added it! I do agree that the casting solution is probably the cleanest, though it's performance still isn't good as some of the other answers. – Monolith Jul 29 '21 at 19:35
  • @Monolith just add an early exit in case nil is found `guard !contains(where: {$0 == nil}) else { return nil }` – Leo Dabus Jul 29 '21 at 20:20
  • Swift functions do not have "orThrow" in their names. That is Hungarian suffix notation, from a time before option-clicking. The labeling belongs on the argument. `orThrow error: @autoclosure () -> Error = FoundNilError()`. –  Jul 30 '21 at 15:15
  • 1
    Thank you for the feedback. What I am mainly suggesting is the optional cast to `[T]`, everything else is just ideas of what is possible. One can surely choose a better name than `unwrapOrThrow` (and you wouldn't call a method `someFunction` either). Btw, the labeling is not always on the argument, it is e.g. `replaceSubrange(_:with:)` and not `replace(subrange:with:)`. – Regarding your suggestion: I am not sure if it is better to introduce an argument just to put a label on it. And such a method would be called using the default argument as `unwrap()`, which *I* find unclear. – Martin R Aug 02 '21 at 20:28
1

Answer does requires at least Swift 5.3

Based on https://forums.swift.org/t/generic-function-that-requires-the-generic-type-to-be-non-optional/30936/11 I came up with:

extension Array {
    @inlinable
    public func someMethod() -> [Any]? {
        first {
            if case Optional<Any>.none = $0 as Any {
                return true
            } else {
                return false
            }
        } != nil ? nil : map {
            if case Optional<Any>.some(let unwrapedValue) = $0 as Any {
                return unwrapedValue
            } else {
                return $0
            }
        }
    }
}

let x: [Int?] = [1,2,3,4]
let y = [1, 2, nil, 3, 4]
let z = [1,2,3,4]

print(x.someMethod())
print(y.someMethod())
print(z.someMethod())

Notice it does not require typing [Int?] for the z case.

Output:

Optional([1, 2, 3, 4])
nil
Optional([1, 2, 3, 4])

Unfortunately I am not able to pull off a where clause which would only apply to nonoptional T. It seems to be possible the other round only for where Element == T? as seen on the examples here. Which prevents me from proper typing to [T] and resorting to [Any].

It's fairly easy for x & y with public func someMethod<T>() -> [T]? where Element == T? . But any attempt at generalizing this for nonoptionals seem to work as a blanket term as a result the public func someMethod<T>() -> [T]? where Element == T? case no longer gets called.

Update Small improvement over aforementioned problem which provides exact typing and less processing for the z case:

extension Array {
    @inlinable
    public func someMethod() -> [Element]? {
        return self
    }
    
    @inlinable
    public func someMethod() -> [Any]? where Element: ExpressibleByNilLiteral {
        first {
            if case Optional<Any>.none = $0 as Any {
                return true
            } else {
                return false
            }
        } != nil ? nil : map {
            if case Optional<Any>.some(let unwrapedValue) = $0 as Any {
                return unwrapedValue
            } else {
                return $0
            }
        }
    }
}
Kamil.S
  • 5,205
  • 2
  • 22
  • 51
-1

CompactMap can be used to drop the nils, then just see if the count is the same.

extension Array {
   func allOrNone<T>() -> [T]? where Element == Optional<T>   {
      let array = compactMap{$0}
      return array.count == count ? array : nil
   }
}
flanker
  • 3,840
  • 1
  • 12
  • 20