読者です 読者をやめる 読者になる 読者になる

Functional Swift 4章 "Map Filter Reduce" まとめ

引数に関数を取る関数を高階関数と呼びます。本章ではSwiftの標準ライブラリに実装されている高階関数を紹介します。

Map, Generic

Int型の配列を受け取り、全ての要素に1を加えて返す関数はfor文を使って簡単に書くことができます。

func incrementArray(xs: [Int]) -> [Int] {
    var result: [Int] = []
    for x in xs {
        result.append(x + 1)
    }
    return result
}

全ての要素を2倍にする関数も同様に書くことができるでしょう。

しかし、これらの関数をより汎用性のあるものにするためには、配列の各要素をとって計算し、Int型を返す関数を第二引数として受けとる必要があります。

func computeIntArray(xs: [Int], transform: Int -> Int) -> [Int] {
    var result: [Int] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}

しかし、このコードはまだ最も柔軟であるとは言えません。 例えば、配列の各要素を偶数であるか判定し、真偽値の値を配列に入れて返したい場合はどうすれば良いでしょうか。

この問題を解決してくれるのがジェネリクスです。

func genericComputeArray<T>(xs: [Int], transform: Int -> T) -> [T] {
    var result: [T] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}

これで任意の型で動作する関数を書くことができました。

この関数はさらに抽象化することができます。入力される配列は[Int]である必要はありません。

func map<Element, T>(xs: [Element], transform: Element -> T) -> [T] {
    var result: [T] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}

グローバルレベルでmap関数を定義するのではなく、Arrayの拡張として定義することでSwiftにうまくフィットします。

extension Array {
    func map<T>(transform: Element -> T) -> [T] {
        var result: [T] = []
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}

map関数はSwiftの標準ライブラリに含まれているので自分で実装する必要はありませんが、map関数はやろうと思えば簡単に実装できることは示したのです。

Filter

あるディレクトリにこんなファイルがあるとします。

let exampleFiles = ["README.md", "HelloWorld.swift", "FlappyBird.swift"]

この中からSwiftファイルだけを取り出したい場合、シンプルな配列で書くことができます。

func getSwiftFiles(files: [String]) -> [String] {
    var result: [String] = []
    for file in files {
        if file.hasSuffix(".swift") {
            result.append(file)
        }
    }
    return result
}

これを抽象化するとFilter関数ができます。

extension Array {
    func filter(includElement: Element -> Bool) -> [Element] {
        var result: [Element] = []
        for x in self where includElement(x) {
            result.append(x)
        }
        return result
    }
}

Reduce

配列内の合計を計算する関数を作るのは簡単です。

func sum(xs: [Int]) -> Int {
    var result: Int = 0
    for x in xs {
        result += x
    }
    return result
}

抽象化してReduce関数ができます。

extension Array {
    func reduce<T>(initial: T, combine: (T, Element) -> T) -> T {
        var result = initial
        for x in self {
            result = combine(result, x)
        }
        return result
    }
}

Putting It All Together

締めとして、map, filter, reduceを使ったささやかな例を紹介します。

都市名と人口を持つ構造体があります。

struct City {
    let name: String
    let population: Int
}

都市をいくつか定義します。

let paris = City(name: "Paris", population: 2241)
let madrid = City(name: "Madrid", population: 3165)
let amsterdam = City(name: "Amsterdam", population: 827)
let berlin = City(name: "Berlin", population: 3562)

let cities = [paris, madrid, amsterdam, berlin]

ここで、100万人以上の人口を持つ都市を人口と一緒に出力したいと思います。

まず、人口の単位を返還するヘルパー関数を定義します。

extension City {
    func cityByScalingPopulation() -> City {
        return City(name: name, population: population * 1000)
    }
}

ここで本章で紹介したすべてのパーツを使って、次のようなコードが書けます。

cities.filter { $0.population > 1000 }
    .map { $0.cityByScalingPopulation() }
    .reduce("") { result, c in
        result + "\n" + "\(c.name): \(c.population)"
    }
Paris: 2241000
Madrid: 3165000
Berlin: 3562000

Swift標準ライブラリの持つmap, filter, reduceをうまくチェーンできました。