iPhone
 Computer >> コンピューター >  >> スマートフォン >> iPhone

SwiftのMap、Filter、Reduceの舞台裏 ― 高階関数の内部実装を徹底解説

関数とは、入力を受け取り、何らかの処理を施して出力を生成するものです。関数はシグネチャ本体から構成され、同じ入力を与えれば必ず同じ出力が得られます。これが関数の簡潔な定義です。

本記事では、Swiftにおける高階関数について掘り下げていきます。別の関数を引数として受け取るか、戻り値として関数を返す関数は「高階関数」と呼ばれます。

Swift開発では、map、filter、reduceを毎日のように使います。これらの関数はまるで魔法のように感じられますが、その裏側で何が起きているのか、正確に理解している方は意外と少ないのではないでしょうか。実は、Map・Filter・Reduceは関数型プログラミングの考え方とアプローチに基づいて動作しています。Swiftは純粋な関数型言語ではありませんが、関数型的な操作を自然に行えるよう設計されています。

それでは、それぞれの関数の裏側で何が起こっているのか、順番に見ていきましょう。まず特定のデータ型向けの基本的なバージョンを実装し、その後、任意の型に対応するジェネリック版の実装へと進化させていきます。

Map関数の仕組み

整数の配列があり、元の配列の各要素にある値(delta)を加算した新しい配列を返す関数が必要だとしましょう。単純なforループを使えば、以下のように書けます。

func increment(by delta: Int, in array: [Int]) -> [Int] {
    var result: [Int] = []
    for element in array {
        result.append(element + delta)
    }
    return result
}

次に、各要素を2倍にした新しい配列を返す関数も必要になりました。この場合も同様に実装できます。

func doubled(in array: [Int]) -> [Int] {
    var result: [Int] = []
    for element in array {
        result.append(element * 2)
    }
    return result
}

この2つの関数を見比べると、基本的に同じことをしていることがわかります。forループの中の変換処理だけが異なり、どちらも整数配列を入力として受け取り、各要素を変換して新しい配列を返しています。つまり本質は「各要素を新しい値へ変換すること」なのです。

Swiftは高階関数をサポートしているため、「配列」と「変換用の関数」を引数として受け取り、各要素に変換関数を適用した結果の新しい配列を返す関数を書けます。

func transform(_ array: [Int],
               using operation: (Int) -> Int) -> [Int] {
    var result: [Int] = []
    for element in array {
        result.append(operation(element))
    }
    return result
}

しかし、まだ問題が残っています。この実装では整数配列しか扱えないのです。例えば、入力の整数配列を文字列配列に変換したい場合には対応できません。そこで必要になるのが、任意の型に対して動作するジェネリック関数です。

Array拡張として、以下の手順でジェネリック版を実装できます。

  1. Array拡張内に、ジェネリック型Tで動作するmap関数を宣言する
  2. 引数として(Element) -> T 型の関数を受け取る
  3. 関数内で、T型のデータを保持する空の結果配列を用意する
  4. 自身をイテレートするforループを実装し、変換関数を呼び出して各要素をT型へ変換する
  5. 変換後の値を結果の配列に追加する
extension Array {
    func myMap(_ transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for element in self {
            result.append(transform(element))
        }
        return result
    }
}

これこそが、Swiftにおけるmap関数の正体です。自分でmapを実装するなら、まさに上記のようになるでしょう。つまり、配列に対して魔法が起きているわけではなく、私たち自身でも簡単に同じ機能を定義できるのです。

Filter関数の仕組み

次に、整数の配列から偶数だけを抽出したいケースを考えてみましょう。単純なforループで実装できます。

func evenNumbers(in array: [Int]) -> [Int] {
    var result: [Int] = []
    for element in array where element % 2 == 0 {
        result.append(element)
    }
    return result
}

今度は、プロジェクトのクラスファイル名を表す文字列の配列から、.swiftファイルだけを残したいとします。これも1つのループで実現できます。

func swiftFiles(in fileNames: [String]) -> [String] {
    var result: [String] = []
    for fileName in fileNames where fileName.hasSuffix(".swift") {
        result.append(fileName)
    }
    return result
}

2つの実装を注意深く観察すると、やはりやっていることは同じで、異なるのは配列のデータ型だけだとわかります。そこで、「配列」と「条件判定用の関数」を引数として受け取り、includeElement関数の出力に応じて要素を結果配列へ追加するかどうかを決める、ジェネリックなfilter関数として一般化できます。

extension Array {
    func myFilter(_ includeElement: (Element) -> Bool) -> [Element] {
        var result: [Element] = []
        for element in self where includeElement(element) {
            result.append(element)
        }
        return result
    }
}

Reduce関数の仕組み

最後に、整数の配列から合計値を計算する2つの関数を実装してみましょう。こちらもforループで書けます。

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

func product(of array: [Int]) -> Int {
    var result = 1
    for element in array {
        result *= element
    }
    return result
}

さらに、整数ではなく文字列の配列があり、すべての要素を連結したい場合はどうでしょう。

func concatenated(_ strings: [String]) -> String {
    var result = ""
    for string in strings {
        result += string
    }
    return result
}

これら3つの関数は、やはり本質的に同じ構造を持っています。配列を入力として受け取り、結果変数を初期化し、配列をイテレートしながら結果変数を更新していく——この共通パターンこそがreduceの核心です。

ここから汎用的な実装を導けます。そのために必要なのは、①結果変数の初期値、そして②毎回のイテレーションで結果変数を更新するための関数の2つです。

extension Array {
    func myReduce(_ initialResult: T,
                     _ nextPartialResult: (T, Element) -> T) -> T {
        var result = initialResult
        for element in self {
            result = nextPartialResult(result, element)
        }
        return result
    }
}

この実装は、[Element]型の任意の入力配列に対してジェネリックに動作し、T型の結果を計算します。動作には、結果変数に代入するT型の初期値と、forループの各イテレーションで結果変数を更新するために使われる(T, Element) -> T型の関数が必要です。

まとめ

map、filter、reduceは決して特別な魔法ではなく、「繰り返し処理のパターンを抽象化した高階関数」にすぎません。それぞれの実装を一度自分の手で書いてみると、Swift標準ライブラリの挙動への理解が深まり、関数型プログラミングへの抵抗感もなくなるはずです。ぜひ実際にコードを書いて試してみてください。

最後までお読みいただきありがとうございました!

  1. Swiftで学ぶ関数合成の基本:カスタム演算子で作る読みやすいコード

    はじめに:関数とは何か プログラマーは毎日のように関数と向き合っています。関数とは、ある入力値に対して必ず何らかの出力値が対応する、特別な種類の関係を表すものです。より一般的に言えば、関数とは「入力値を出力値へ対応付けるルール」だと言えます。 関数合成(Function Composition)とは 関数合成の基本的な考え方は、「ある関数を、別の関数の結果に対して適用する」というものです。つまり、複数の関数を1つの関数へと組み合わせる数学的な概念です。 関数合成のイメージ 数学的な基礎から理解する まずは数学の概念とともに見ていきましょう。上の図では「f」と「g」という2つの関数が登場し

  2. SwiftのGenericsとAny型の違いを徹底解説!型安全なコードを書くために知っておきたいポイント

    Swiftは、現代において最も型安全なプログラミング言語の一つとして広く認知されています。そもそも「型安全」とは何か?型安全(タイプセーフ)な言語とは、常に操作がその時点で利用可能な正しい種類のデータに対して行われることを保証する言語のことです。具体的には、Int、Float、String、Array、Dictionaryといった異なるデータ型を宣言できる能力を持ち、かつ特定のデータ型で宣言された変数が、決して別の種類のデータを保持しないことを保証できる言語を指します。型安全な言語では必ず型チェックが行われます。このチェックは言語によって異なり、コンパイル時に行われる場合もあれば、ランタイム時