Warning: this article is out of date as of Swift 1.0 beta 4. Read this updated version instead.
The Swift non-member function map
acts much like the Array.map
member function, except you can pass in any kind of sequence or collection (click here for an article on how to write an algorithm like this).
But if you look at what map
returns, it isn’t an array of results. It’s an object (a MapColectionView
or a MapSequenceView
).
That’s because map
evaluates its results lazily. When you call map
, no mapping takes place. Instead, the input and mapping function are stored for later, and values are only mapped as and when they are needed.
To see this happening, run the following code:
let r = 1...3 let mapped = map(r) { (i: Int) -> Int in println("mapping (i)") return i*2 } println("crickets...") for i in mapped { println("mapped (i)") } println("tumbleweed...") println("index 2 = (mapped[2])") // prints out crickets... mapping 1 mapped 2 mapping 2 mapped 4 mapping 3 mapped 6 tumbleweed... mapping 2 index 2 = 4
The returned object from map
mimics the properties from the object passed in. If you pass in a sequence, then you get back a sequence. If the collection you passed in only supported a bidirectional index (such as a Dictionary or a String, which can’t be indexed randomly), then you won’t be able to subscript arbitrarily into the mapped collection, you’ll have to walk through it.
map
is not the only lazy function. filter
and reverse
also return these “view” classes. Combining these different lazily evaluated classes means you can build up quite complicated expressions without sacrificing efficiency.
Want to search backwards through an array or string? find(reverse(myarray), value)
won't actually reverse the array to search through it, it just iterates over the ReverseView
class, which serves up the array in reverse order.
Want the first 3 odd numbers that are a minimum of two arrays at each position? first_n(filter(map(Zip2(a1, a2), min) {$0%2 == 1}, 3)
wont do any more zipping or minning or modding than necessary.123
There are also several lazily generated sequences you can also use:
enumerate
numbers each item in a collection — it returns an EnumerateGenerator
, that serves up those numbered elements on demand. This is useful in a for (count, val) in enumerate(seq)
loop where you want to know both the value and how many values so far.
PermutationGenerator
is initialized with a collection and a collection of indices into that collection, and serves up each element at each index in that order (thus allowing you to lazily reorder any collection – for example, PermutationGenerator(elements: a, indices: reverse(a.startIndex..
<a.endIndex))
would be the reverse of a
.4
GeneratorOf
just takes a closure and runs it each time to generate the next value. So var i = 0;var naturals = GeneratorOf({ ++i })
sets naturals
to a sequence that counts up from 1 to infinity (or rather, to overflow).
These infinite virtual sequences can be pretty useful. For example, to do the same thing as enumerate
, you could write Zip2(naturals, somecollection)
. Or you could pass your infinite sequence into map
or filter
to generate further inifinite sequences.
(at the opposite end, if you have a single value, but what a function needs is a Sequence
, you can use GeneratorOfOne(value)
to create a quick sequence that will just serve up just your one value)
It's not all rainbows and unicorn giggles though. Imagine you did the following:
let r = 1...10_000 mapped = map(r) { megaExpensiveOp($0) } let a1 = mapped let a2 = mapped
Here the lazy evaluation will work against you. megaExpensiveOp
will be run not ten but twenty thousand times.
“Shouldn't map
cache the data?” you ask. Well that leads to the next complication. Take this code:
var i =0 let mapped = map(1...5) { i += 1 return $0 + i }
Every time you iterate over mapped
now, you'll get different results.5
This behaviour might be put to very good use (say you wanted to peturb a data set with small random values). But if you weren't expecting it, that could be one nasty bug to track down.
So its good to know when you're using these functions what they are actually doing under the hood. Just keep in mind these caveats and an exciting life of lazy evaluation awaits you in the off-world colonies.
-
OK I cheated,
first_n
isn't a Swift standard library function. But it should be! ↩ - This code would look a lot nicer if Swift came with a pipe operator ↩
- Yeah, this is a pretty rubbish example, I should have put the time in to think of a better real-life use case. ↩
-
Obviously,
reverse(a)
would work better, but a more useful example is tough to fit on one line… ↩ - If you're unclear on how come, try reading this article. ↩