LoginSignup
53
53

More than 5 years have passed since last update.

SwiftのFunctional Programmingっぽい書き方を理解する

Last updated at Posted at 2016-01-26

はじまり

Swiftを勉強をする上で避けて通れないFunctional Programmingについてfilterreduceを使ったり、作ったりして勉強してみます。※ おおよそ参考サイトから引用しているので分かりづらければ元サイトを見てください。

実施環境

  • Xcode Version 7.2 (Playground)
  • Swift version 2.1.1

配列をフィルタリングする

今までのやり方

1から10の数値から偶数のみ取り出した配列を生成したい場合、今まではこのような処理を行っていたと思います。

var evens = [Int]()
for number in 1...10 {
    if number % 2 == 0 {
        evens.append(number)
    }
}
print(evens) // [0, 2, 4, 6, 8, 10]

まず偶数の数値を格納する配列を用意してfor-inで10回まわして、条件に当てはまる場合は用意した配列に追加する…

上記の方法は内部の構造が明示的ですがコードの再利用を行うとしたらコピペに頼るしかなくなります。

Functionalなやり方

func isEven(number: Int) -> Bool {
    return number % 2 == 0
}
let evens = Array(1...10).filter(isEven)
print(evens) // [0, 2, 4, 6, 8, 10]

先ほどの条件処理部分を抜き出したisEvenは偶数であればtrueを返す単純な関数です。それを引数に渡しているのが肝です。

filter与えられた関数trueを返す時だけ配列の中身を取り出し、新たな配列を返す関数です。

またこの引数として渡す関数はクロージャとしても宣言できます。

let evens = Array(1...10).filter { (number) -> Bool in
    return number % 2 == 0
}
print(evens) // [0, 2, 4, 6, 8, 10]

さらに戻り値の型推論で-> Boolが必要なく、クロージャは最後の値を戻り値としてを暗黙に返すのでreturnが必要なく、引数も自動的に$0, $1, ...と連番が与えられるのでnumber inも必要なくなります。

つまり残ったのはこれだけ、最近よく見る書き方になりました。

let evens = Array(1...10).filter { $0 % 2 == 0 }
print(evens) // [0, 2, 4, 6, 8, 10]

※しかし略した記法は可読性を下げる可能性が高く、好みが分かれるのでチームで開発する際にはコーディング規則に則って行いましょう。

話が少し逸れてしまいましたが、この関数を引数として受け取る関数は高階関数と言いい、Functional Programmingの基本となります。またクロージャを使用することもできます。

フィルターを自作する

Swiftの配列CollectionType(親はSequenceType)に準拠した型のインスタンスは上記filterの他にmapSequenceTypeであればreduceなど多くのFunctionalな関数を用意しています。その使い方を知りたい場合は下記リンクが分かりやすいのでオススメです。

Swiftのmap, filter, reduce(などなど)はこんな時に使う!

ではそのfilterがどのように実装しているのか理解するために、自作の高階関数を定義してみましょう。

func goodFilter<T>(source: [T], predicate:(T) -> Bool) -> [T] {
    var result = [T]()
    for i in source {
        if predicate(i) {
            result.append(i)
        }
    }
    return result
}

let evens = goodFilter(Array(1...10)) { $0 % 2 == 0 }
print(evens) // [0, 2, 4, 6, 8, 10]

引数としてある型で宣言された配列と同じ型のインスタンスを受け取ってブール値を返す関数を受け取り、その返り値に同じ型の配列を返すというジェネリクス関数です。

中の処理は最初に書いたコードと同じです。filterと同じ結果が出力されていると思います。

またArraySequenceTypeextensionとして宣言すれば通常のfilterと同様にドット繋ぎで使用することができます。

配列の合計値を計算する

今までのやり方

次は少し複雑な処理として、先ほどフィルタリングした配列の合計値を計算してみましょう。

var evens = [Int]()
for number in 1...10 {
    if number % 2 == 0 {
        evens.append(number)
    }
}

var evenSum = 0
for number in evens {
    evenSum += number
}

print(evenSum) // 30

フィルタリングの処理は最初に使用したコードと同じです。合計値を出す処理もまた変数を用意してfor-inでまわして足し合わせたものを変数に入れるといったものです。

Functionalなやり方

let evenSum = Array(1...10).filter { $0 % 2 == 0 }
    .reduce(0) { (total, number) -> Int in
        return total + number
}

print(evenSum) // 30

まずfilterで処理された[0, 2, 4, 6, 8, 10]が渡されています。reduceは初期値と配列の各要素に対して行われる関数で結合された値を返す高階関数です。

前回同様省略できる箇所を削ると以下のような書き方になります。

let evenSum = Array(1...10).filter { $0 % 2 == 0 }
    .reduce(0) { $0 + $1 }

print(evenSum) // 30

追記{ $0 + $1 }は2つの引数を取る関数(クロージャ)ですが+自体が2引数で返り値のある関数なのでこういった書き方もできます。@ken0nekさんご教授ありがとうございました。

(0...10).filter { $0 % 2 == 0 }.reduce(0, combine: +)

追記:また今回は配列(Array)を指定していますが、実際filterreduceSequenceTypeのprotocolで定義されているので上記の(1...10).filterのようにRangeで直接呼び出すことができます。@yuta-tさんご教授ありがとうございました。

今回は少し複雑なのでreduceのシグネチャを見てみましょう。

func reduce<U>(initial: U, combine: (U, T) -> U) -> U

最初のパラメータinitialは今回合計値を計算するのでInt型の0が入りました。次のcombineで結合する処理を行っていることが分かります。

combineでは二つの引数を取り、一つ目の引数は前にcombineで返された値(最初は初期値)が渡されます。二つ目の引数には配列の各要素が渡されます。そしてreduceの返り値にはcombineで最後に処理された値が返されます。

文章で書くと分かりづらいですよね。そう言えば頭の良い人が「reduceはパックマンがエサを喰っているのをイメージするといい」って言ってました。

[1, 8, 2, 5]の配列を上記の合計値を計算させるreduceにかける時のイメージです。最初にinitialというエサを食べたパックマンが動き出します…

packman-1.png

このパックマン達は食べると単純に食べたエサ分太る(足す)という性質を持っています。

packman-2.png

太ったパックマンはそのまま次のエサを食べてまた太ります…

packman-3.png

これを最後のエサまで続けます…

packman-4.png

そして体内に蓄えた返り値を返してきます…

packman-5.png

厳密に言うとパックマンは毎回自身の返り値を食べてからエサを食べるというのが正しいと思うのですが、絵にしたくないのでご了承ください…

その他の使用方法

reduceは配列の要素を全てなめて何かしらの結果を返す処理をするにはとても強力な関数です。以下に例を示します。

let maxNumber = [1, 8, 2, 5].reduce(0) { max($0, $1) }
print(maxNumber) // 8

これは配列の要素の中で一番大きな値を取り出す処理です。前の要素(最初は初期値)と後の要素をmax関数で比べて最後に返された値がmaxNumberに代入されます。

let numbers = [1, 8, 2, 5].reduce("numbers: ") { $0 + "\($1) "}
print(numbers) // numbers: 1 8 2 5

Int型の配列の要素をString型の文字列連結して返すこともできます。初期値には最初に表示したい文字列を入れています。

リデュースを自作する

ではfilterと同様にreduceを自作してみましょう。今回はArraySequenceTypeextensionを使っているのでドットで呼び出せます。

extension SequenceType {
    func goodReduce<T, U>(initial:U, combiner:(U, T) -> U) -> U {
        var current = initial
        for item in self {
            if let item = item as? T {
                current = combiner(current, item)
            }
        }
        return current
    }
}

let maxNumber = [1, 8, 2, 5].goodReduce(0) { current, number in max(current, number) }
print(maxNumber) // 8

まず初期値のinitialの型Uと配列の各要素の型Tを宣言しています。中の処理としては前述した通り、各要素のitemがある限りcurrentcombiner関数の返り値を入れていくという単純な処理です。

extension SequenceType {
    func goodReduce<U>(initial:U, combiner:(U, Element) -> U) -> U {
        var current = initial
        for item in self {
            current = combiner(current, item)
        }
        return current
    }
}

let maxNumber = [1, 8, 2, 5].goodReduce(0) { max($0, $1) }
print(maxNumber) // 8

余談ですが先ほどのTで宣言していた型をGeneratorTypeプロトコルに準拠したElementを使用することで引数に$0, $1, ...の連番を与えられ、型のチェックも必要なくなります。

GeneratorTypeについて勉強中で、何故そうなるのか詳しく分かっていません。ご教授お願いいたします…

おしまい

途中話が逸れているような気がしますがが、何となくSwiftのFunctional Programmingっぽい書き方に慣れることができました。

参考サイト

CollectionType Protocol Reference
SequenceType Protocol Reference
Swift Functional Programming Tutorial

53
53
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
53