Swift’s map functions provide elegant list processing that save you time when coding. Use them for good and not evil.
Mappage
Basic mapping passes each element of an array to a closure. Closures act on and transform elements. If you start with [x1, x2, x3, …], you end up with [f(x1), f(x2), f(x3), …].
/// Return an `Array` containing the results of calling /// `transform(x)` on each element `x` of `self` func map<U>(transform: (T) -> U) -> [U]
For example you might use map to count the letters of each word in a string and print the results:
let lipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." let words = split(lipsum, isSeparator:{$0 == " "}) let counts = words.map{count($0)} for pair in zip(words, counts){println("\(pair.0): \(pair.1)")}
If you want to save a little overhead, use lazy. It returns a sequence or collection type that can be mapped without first producing an array. (Thanks, Lily Ballard)
let counts = lazy(words).map(count)
If you’re willing to sacrifice dignity, map also works for low-rent enumeration:
(1...5).map{_ in println("Hello")}
Flatmap on the other hand, transforms each element into an array. You output [U] instead of U from your closure. Flatmap then smooshes all those arrays together at the top level to create its output.
/// Return an `Array` containing the results of calling /// `transform(x)` on each element `x` of `self` and flattening the result. func flatMap<U>(transform: @noescape (T) -> [U]) -> [U]
So, for example
[[1], [2, 3], [4], [5, 6, 7]].flatMap{$0}
returns [1, 2, 3, 4, 5, 6, 7] after smooshing.
Combinage
Flatmap’s perfect for when you’re working with any kind of function that returns collections grown from subresults. If you would have normally used arrayByAddingObjectsFromArray in Objective-C, you’ll probably want to use flatmap in Swift.
For example you might use it for collecting sectionIndexTitles from your fetchedResultsController or constraints from format-based NSLayoutConstraint creation or arrays of subviews from recursive tree descent.
Flatmap elegantly unifies component arrays into a single array, like you find when you install a group of layout formats all at once:
let constraints = ["H:|->=inset-[view]", "H:[view]->=inset-|", "V:|->=inset-[view]", "V:[view]->=inset-|"].flatMap { NSLayoutConstraint.constraintsWithVisualFormat( $0, options: options, metrics: metrics, views: bindings)}
Denilage.
Flatmap also helps sort the wheat from the chaff. In this example, it combs an array for all non-nil elements:
let optionalWords : [String?] = ["bob", nil, "eats", nil, "burgers"] println(optionalWords.flatMap{ (item : String?) -> [String] in return item == nil ? [] : [item!]})
Unwrappage Mappage
Both map and flatmap work with optional values as well as arrays. Their calls are similar as you see in the following declarations, but map closures return type U, which may or may not be an optional, while flatmap closures specifically return type U?, which is always an optional.
/// If `self == nil`, returns `nil`. Otherwise, returns `f(self!)`. func map<U>(f: @noescape (T) -> U) -> U? /// Returns `f(self)!` iff `self` and `f(self)` are not nil. func flatMap<U>(f: @noescape (T) -> U?) -> U?
When you just want to unwrap-and-apply, use map. This mapping
UIImage(named:"snail").map{println($0.size)}
is equivalent to this if-let
if let image = UIImage(named:"snail") { println(image.size) }
And, honestly, both include about the same level of code complexity for this particular example. Both unwrap the optional returned by UIImage(named:) and then print the size. Skipped unwrapping means working with optional results. For example, both
let image = UIImage(named:"snail") println(image?.size)
and
println(UIImage(named:"snail")?.size)
print results typed Swift.Optional<C.CGSize>. These results look like “Optional((168.0, 111.0))” instead of the desired “(168.0, 111.0)”. Although optional chaining allows you to access properties, those value appear in optional wrappers.
UnwrappageFlatmappage
What you really don’t want to do is chain multiple maps together. Cascaded if-let statement like this handle multi-stage optionals beautifully.
let urlString = "http://jsonplaceholder.typicode.com/todos/1" if let url = NSURL(string:urlString), data = NSData(contentsOfURL: url), json = NSJSONSerialization.JSONObjectWithData( data, options: NSJSONReadingOptions(0), error: nil) as? NSDictionary, title = json["title"] as? String { println(title) }
Maps do not. You get about as far as this:
let result = NSURL(string:urlString).map{NSData(contentsOfURL: $0)}
before you’re dealing with a result type of NSData?? (yes, two question marks) and things rapidly get worse from there.
Instead, if you truly must use chaining, use flatMap, which irons out excess optionals as it flows. Here’s the flatMap equivalent of the if-let cascade, in all its hideous glory. Cover the eyes of any children who may be near you, reading over your shoulder:
NSURL(string:urlString).flatMap{ NSData(contentsOfURL: $0)}?.flatMap{ NSJSONSerialization.JSONObjectWithData( $0, options: NSJSONReadingOptions(0), error: nil) as? NSDictionary}.flatMap{ $0["title"]}.flatMap{ println($0)}
Ugly, isn’t it?
Reserve maps (flat or pointy) for short, quick unwrapping hits or for their proper and godly intended purpose of applying f(x) across an array.
So relax. Just because you can flatmap flapjacks in your thorax backpacks doesn’t mean you should flatmap flapjacks in your thorax backpacks.
p.s. Want to flatten bunches of fields at a time but in parallel not serial fashion? Switches. But that must wait for another write-up.
Waves hi to all the people showing up from Hacker News. Thanks for stopping by!
2 Comments
How about this:
“`swift
import Runes
NSURL(string:urlString)
>>- { NSData(contentsOfURL: $0) }
>>- { NSJSONSerialization.JSONObjectWithData(
$0, options: NSJSONReadingOptions(0), error: nil
) as? NSDictionary }
>>- { $0[“title”] }
>>- { println($0) }
“`
I think this is easier to read than cascaded if-let statements because there are fewer names to keep track of and you know immediately for each line which value was passed on from the previous line ($0), you don’t have to read the line before to find its name. Of course sometimes the additional names can help clarify the code, but in this case I think it’s pretty self-explanatory.
Properly formatted of course but hopefully you know what I mean.