Flat Map

Episode #143 | 25 minutes | published on October 30, 2014 | Uses swift-1.0
Subscribers Only
In this episode we cover a horribly named, yet fairly powerful concept called flat map. We'll use this technique to solve the problem we discovered last time dealing with Result<T> and having to use a switch statement everywhere.

Episode Links

Adding Map on Result

enum Result<T> {
    case Success(Box<T>)
    case Error(String)

    func map<U>(transform: T -> U) -> Result<U> {
        switch self {
        case .Success(let value):
            return .Success(Box(transform(value.unbox)))
        case .Error(let error):
            return .Error(error)
        }
    }
}

Now we have an easy way to apply a result to another function, provided it accepts the T we're returning from the .Success case. The only problem here now is that we can end up with a Result<Result<T>> which doesn't sound like much fun to work with.

We can solve this by flattening the value.

Flattening Arrays

It's easier to see this with an example of nested arrays:

var customers = [
    Customer(name: "Alice", emails: ["alice@aol.com"]),
    Customer(name: "Bob", emails: ["bob@msn.com", "bob@gmail.com"]),
    Customer(name: "Charlie", emails: ["c@example.com"])
    ]

func flatten<T>(array: [[T]]) -> [T] {
    return array.reduce([]) { $0 + $1 }
}

extension Array {
    func flatMap<U>(transform: T -> [U]) -> [U] {
        return flatten(self.map(transform))
    }
}

Applying this to Result looks like this:

func flatten<T>(result: Result<Result<T>>) -> Result<T> {
    switch result {
    case .Success(let box):
        switch box.unbox {
        case .Success(let value): return .Success(value)
        case .Error(let error): return .Error(error)
        }
    case .Error(let error): return .Error(error)
    }
}

extension Result {
    func flatMap<U>(transform: T -> Result<U>) -> Result<U> {
        return flatten(self.map(transform))
    }
}

You can see it looks very similar. Usage looks like this:

func divide(x: Int, y: Int) ->  Result<Float> {
    if y == 0 {
        return .Error("divide by zero")
    } else {
        return .Success(Box(Float(x) / Float(y)))
    }
}

func compute(input: Float) -> Result<Float> {
    if input < 0 {
        return .Error("negative")
    }
    return .Success(Box(log(input)))
}

switch divide(12, 2).flatMap(compute) {
case .Success(let value): value.unbox
case .Error(let error): error
}

A more complicated example and an operator

If you read Rob's post, he does a good job justifying why an operator might be helpful for this particular function, but not to go overboard with operators. Here's his completed example:

class Page {
    var content: String

    init(content: String) {
        self.content = content
    }
}

func asJSON(data: NSData) -> Result<AnyObject> {
    var error: NSError?
    if let json: AnyObject = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: &error) {
        return .Success(Box(json))
    } else {
        return .Error("couldn't parse as JSON: \(error)")
    }
}

func asJSONArray(any: AnyObject) -> Result<NSArray> {
    if let array = any as? NSArray {
        return .Success(Box(array))
    } else {
        return .Error("wasn't an array")
    }
}

func secondElement(input: NSArray) -> Result<AnyObject> {
    if input.count >= 2 {
        return .Success(Box(input[1]))
    } else {
        return .Error("list only had \(input.count) elements")
    }
}

func asStringList(any: AnyObject) -> Result<[String]> {
    var list = [String]()
    if let array = any as? NSArray {
        for item in array {
            if let string = item as? String {
                list.append(string)
            } else {
                return .Error("Element \(item) was not a string")
            }
        }
        return .Success(Box(list))
    } else {
        return .Error("element \(any) was not an array")
    }
}

func asPages(contents: [String]) -> Result<[Page]> {
    let pages = map(contents) { Page(content: $0) }
    return .Success(Box(pages))
}

var json = "[ \"a\", [ \"page1\", \"page2\", \"page3\"] ]"
var data: NSData = json.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)!


infix operator >>== {
associativity left
}

func >>==<T, U>(lhs: Result<T>, rhs: (T -> Result<U>)) -> Result<U> {
    return lhs.flatMap(rhs)
}


func pagesFromData(data: NSData) -> Result<[Page]> {
    //    let pages = asJSON(data)
    //        .flatMap(asJSONArray)
    //        .flatMap(secondElement)
    //        .flatMap(asStringList)
    //        .flatMap(asPages)

    let pages = asJSON(data)
        >>== asJSONArray
        >>== secondElement
        >>== asStringList
        >>== asPages

    return pages
}


switch pagesFromData(data) {
case .Success(let pages): pages.unbox
case .Error(let error): error
}

blog comments powered by Disqus