50

The language guide has revealed no trace of list comprehension. What's the neatest way of accomplishing this in Swift? I'm looking for something similar to:

evens = [ x for x in range(10) if x % 2 == 0]
Palpatim
  • 9,074
  • 35
  • 43
Maria Zverina
  • 10,863
  • 3
  • 44
  • 61
  • 1
    This is related to Scala (but very readable): http://stackoverflow.com/a/1059501/1153435 – Eduardo Jun 03 '14 at 03:05
  • @Eduardo that's a quite good, informative answer, but it doesn't really address the question (...accomplishing this *in Swift*). – User Jun 28 '15 at 23:36
  • 1
    @Ixx: this question was asked when Swift was just launched, when there was no Swift documentation other than Apple's, and, I believe, it was worded differently (a bit more generic). – Eduardo Jun 29 '15 at 05:01
  • How about `let celsiusValues = (-100...100).map{$0}` – onmyway133 Oct 14 '15 at 04:31
  • See my [answer for Swift 3](https://stackoverflow.com/a/34927738/1966109) that shows up to 7 different ways to solve your problem. – Imanou Petit Jun 29 '17 at 11:03

8 Answers8

68

As of Swift 2.x, there are a few short equivalents to your Python-style list comprehension.

The most straightforward adaptations of Python's formula (which reads something like "apply a transform to a sequence subject to a filter") involve chaining the map and filter methods available to all SequenceTypes, and starting from a Range:

// Python: [ x for x in range(10) if x % 2 == 0 ]
let evens = (0..<10).filter { $0 % 2 == 0 }

// Another example, since the first with 'x for x' doesn't
// use the full ability of a list comprehension:
// Python: [ x*x for x in range(10) if x % 2 == 0 ]
let evenSquared = (0..<10).filter({ $0 % 2 == 0 }).map({ $0 * $0 })

Note that a Range is abstract — it doesn't actually create the whole list of values you ask it for, just a construct that lazily supplies them on demand. (In this sense it's more like Python's xrange.) However, the filter call returns an Array, so you lose the "lazy" aspect there. If you want to keep the collection lazy all the way through, just say so:

// Python: [ x for x in range(10) if x % 2 == 0 ]
let evens = (0..<10).lazy.filter { $0 % 2 == 0 }
// Python: [ x*x for x in range(10) if x % 2 == 0 ]
let evenSquared = (0..<10).lazy.filter({ $0 % 2 == 0 }).map({ $0 * $0 })

Unlike the list comprehension syntax in Python (and similar constructs in some other languages), these operations in Swift follow the same syntax as other operations. That is, it's the same style of syntax to construct, filter, and operate on a range of numbers as it is to filter and operate on an array of objects — you don't have to use function/method syntax for one kind of work and list comprehension syntax for another.

And you can pass other functions in to the filter and map calls, and chain in other handy transforms like sort and reduce:

// func isAwesome(person: Person) -> Bool
// let people: [Person]
let names = people.filter(isAwesome).sort(<).map({ $0.name })

let sum = (0..<10).reduce(0, combine: +)

Depending on what you're going for, though, there may be more concise ways to say what you mean. For example, if you specifically want a list of even integers, you can use stride:

let evenStride = 0.stride(to: 10, by: 2) // or stride(through:by:), to include 10

Like with ranges, this gets you a generator, so you'll want to make an Array from it or iterate through it to see all the values:

let evensArray = Array(evenStride) // [0, 2, 4, 6, 8]

Edit: Heavily revised for Swift 2.x. See the edit history if you want Swift 1.x.

rickster
  • 124,678
  • 26
  • 272
  • 326
  • 1
    I'm glad that list comprehension ops made their way to Apple development. The ObjC-style collection manipulation was awful, long and error-prone. – Maciej Jastrzebski Jun 03 '14 at 18:44
  • 4
    You can also avoid creating an intermediate array with `Array(filter(1..10) { $0 % 2 == 0 })` – ilya n. Jun 05 '14 at 00:08
  • The reduce idiom passes compiler when I add the initial value: let sum = reduce(1..10, 0) { $0 + $1 } – rholmes Jun 14 '14 at 17:49
  • Note the range operator has changed to '..<' in the latest beta. Also, the Array() constructors are optional. – Stephen Petschulat Jul 23 '14 at 22:09
  • Thanks for the reminder to edit my answer. As I've noted, the `Array()` constructor forces evaluation of the entire sequence -- otherwise, you're left with a generator object that produces new elements only on demand. So it's optional in the sense that the code can do something useful without it... but if you want a construct that produces a static list instead of a lazily built one, you'll need to convert the generator to an array. – rickster Jul 23 '14 at 23:57
  • Is there something similar to `propertyList = [item.property for item in items]`? – huggie Feb 22 '16 at 08:44
  • That's a map, same as the `people` isAwesome` example above with but with fewer other actions along the way: `propertyList = items.map { $0.property }`. – rickster Feb 22 '16 at 19:15
20

With Swift 5, you can choose one of the seven following Playground sample codes in order to solve your problem.


#1. Using stride(from:to:by:) function

let sequence = stride(from: 0, to: 10, by: 2)
let evens = Array(sequence)
// let evens = sequence.map({ $0 }) // also works
print(evens) // prints [0, 2, 4, 6, 8]

#2. Using Range filter(_:) method

let range = 0 ..< 10
let evens = range.filter({ $0 % 2 == 0 })
print(evens) // prints [0, 2, 4, 6, 8]

#3. Using Range compactMap(_:) method

let range = 0 ..< 10
let evens = range.compactMap({ $0 % 2 == 0 ? $0 : nil })
print(evens) // prints [0, 2, 4, 6, 8]

#4. Using sequence(first:next:) function

let unfoldSequence = sequence(first: 0, next: {
    $0 + 2 < 10 ? $0 + 2 : nil
})
let evens = Array(unfoldSequence)
// let evens = unfoldSequence.map({ $0 }) // also works
print(evens) // prints [0, 2, 4, 6, 8]

#5. Using AnySequence init(_:) initializer

let anySequence = AnySequence<Int>({ () -> AnyIterator<Int> in
    var value = 0
    return AnyIterator<Int> {
        defer { value += 2 }
        return value < 10 ? value : nil
    }
})
let evens = Array(anySequence)
// let evens = anySequence.map({ $0 }) // also works
print(evens) // prints [0, 2, 4, 6, 8]

#6. Using for loop with where clause

var evens = [Int]()
for value in 0 ..< 10 where value % 2 == 0 {
    evens.append(value)
}
print(evens) // prints [0, 2, 4, 6, 8]

#7. Using for loop with if condition

var evens = [Int]()
for value in 0 ..< 10 {
    if value % 2 == 0 {
        evens.append(value)
    }
}
print(evens) // prints [0, 2, 4, 6, 8]
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
4

Generally, a list comprehension in Python can be written in the form:

[f(x) for x in xs if g(x)]

Which is the same as

map(f, filter(g, xs))

Therefore, in Swift you can write it as

listComprehension<Y>(xs: [X], f: X -> Y, g: X -> Bool) = map(filter(xs, g), f)

For example:

map(filter(0..<10, { $0 % 2 == 0 }), { $0 })
Ray
  • 1,647
  • 13
  • 16
  • 1
    no, Python's comprehensions allow for multiple generator expressions, hence you get a cartesian product which returns a list of tuples, and then you apply the filter and map expressions. – randomsurfer_123 Aug 12 '16 at 14:32
3

As of Swift 2 you can do something like this:

var evens = [Int]()
for x in 1..<10 where x % 2 == 0 {
    evens.append(x)
}

// or directly filtering Range due to default implementations in protocols (now a method)
let evens = (0..<10).filter{ $0 % 2 == 0 }
Qbyte
  • 12,753
  • 4
  • 41
  • 57
  • for me the filter on the range doesn't seem to work in Swift 2. When tried in a playground, it always prints 8. Any ideas why? – Salman Hasrat Khan Oct 30 '15 at 07:59
  • @SalmanHasratKhan I use Xcode 7.1 and it works flawlessly (Swift 2.1). – Qbyte Nov 01 '15 at 15:27
  • don't know why it doesn't work in the playground for me, but it does work in the project itself. Strange. – Salman Hasrat Khan Nov 02 '15 at 03:18
  • 2
    @SalmanHasratKhan It could be that the playground only displays the last computed value of the closure. So you can right click on the display box and choose "value history". – Qbyte Nov 02 '15 at 06:24
1

Got to admit, I am surprised nobody mentioned flatmap, since I think it's the closest thing Swift has to list (or set or dict) comprehension.

var evens = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].flatMap({num -> Int? in 
    if num % 2 == 0 {return num} else {return nil}
})

Flatmap takes a closure, and you can either return individual values (in which case it will return an array with all of the non-nil values and discard the nils) or return array segments (in which case it will catenate all of your segments together and return that.)

Flatmap seems mostly (always?) to be unable to infer return values. Certainly, in this case it can't, so I specify it as -> Int? so that I can return nils, and thus discard the odd elements.

You can nest flatmaps if you like. And I find them much more intuitive (although obviously also a bit more limited) than the combination of map and filter. For example, the top answer's 'evens squared', using flatmap, becomes,

var esquares = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].flatMap({num -> Int? in 
    if num % 2 == 0 {return num * num} else {return nil}
})

The syntax isn't quite as one-line not-quite-the-same-as-everything-else as python's is. I'm not sure if I like that less (because for the simple cases in python it's very short and still very readable) or more (because complex cases can get wildly out of control, and experienced python programmers often think that they're perfectly readable and maintainable when a beginner at the same company can take half an hour to puzzle out what it was intended to do, let alone what it's actually doing.)

Here is the version of flatMap from which you return single items or nil, and here is the version from which you return segments.

It's probably also worth looking over both array.map and array.forEach, because both of them are also quite handy.

Adam Lang
  • 49
  • 4
1

One aspect of list comprehension that hasn't been mentioned in this thread is the fact that you can apply it to multiple lists' Cartesian product. Example in Python:

[x + y for x in range(1,6) for y in range(3, 6) if x % 2 == 0]

… or Haskell:

[x+y | x <- [1..5], y <- [3..5], x `mod` 2 == 0]

In Swift, the 2-list equivalent logic is

list0
    .map { e0 in
        list1.map { e1 in
            (e0, e1)
        }
    }
.joined()
.filter(f)
.map(g)

And we'd have to increase the nesting level as the number of lists in input increases.

I recently made a small library to solve this problem (if you consider it a problem). Following my first example, with the library we get

Array(1...5, 3...5, where: { n, _ in n % 2 == 0}) { $0 + $1 }

The rationale (and more about list comprehension in general) is explained in an blog post.

Daniel Duan
  • 2,493
  • 4
  • 21
  • 24
0

One way would be :

var evens: Int[]()
for x in 0..<10 {
    if x%2 == 0 {evens += x} // or evens.append(x)
}
nschum
  • 15,322
  • 5
  • 58
  • 56
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
0

Here's an extension to the Array types that combines filter and map into one method:

extension Array {

    func filterMap(_ closure: (Element) -> Element?) -> [Element] {

        var newArray: [Element] = []
        for item in self {
            if let result = closure(item) {
                newArray.append(result)
            }
        }
        return newArray
    }

}

It's similar to map except you can return nil to indicate that you don't want the element to be added to the new array. For example:

let items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let newItems = items.filterMap { item in
    if item < 5 {
        return item * 2
    }
    return nil
}

This could also be written more concisely as follows:

let newItems = items.filterMap { $0 < 5 ? $0 * 2 : nil }

In both of these examples, if the element is less than 5, then it is multiplied by two and added to the new array. If the closure returns nil, then the element is not added. Therefore, newIems is [2, 4, 6, 8].

Here's the Python equivalent:

items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
newItems = [n * 2 for n in items if n < 5]
Peter Schorn
  • 916
  • 3
  • 10
  • 20