Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 631 - Arrayの標準メソッド:map, filter, reduce, joined

2022年9月12日:Swift5.7で問題無く動くことを確認
2017年5月27日:joined(separator:)の説明を加える*1

CoreDataの勉強中ですが、まだまだ基本的なところを抑えきれていないので今日はアレー型の持っている標準メソッドをまとめます。

まあ、基本的にこのページで説明されている内容をXcode7.3を使って確認しただけです(^_^;) -> Xcode 8で動くように改訂しました。

アレー型が持っている重要なメソッド「map」は以前みたことがあります。あの頃はちゃんと理解できていなかったのですが、クロージャの概念が分かった今は理解できます。
アレー型が持っている標準メソッドで頻発の3つのメソッドをまとめます。

map

var mapNumbers = 
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .map({(a: Int) -> Int in return a * 3})

実は引数として与えるクロージャは複数パターンの省略が可能なので混乱しそうになります。

  • 戻り値の型の省略をしたケースが次です。
var mapNumbers = 
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .map({(a: Int) in return a * 3})
  • そしてreturnも省略できるので記述はシンプルになります。
var mapNumbers = 
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .map({(a: Int) in a * 3})
var mapNumbers = 
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .map({a in a * 3})
  • 引数を省略して「$0」を使うともっとシンプルになります
var mapNumbers = 
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .map({$0 * 3})
  • Trailing Closure」に変更できます。
var mapNumbers = 
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .map() { $0 * 3 }
  • こうなると「()」も省略できます。
var mapNumbers = 
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    .map { $0 * 3 }

元の配列内容を変更(この場合3倍します)して配列を返します。どのケースでも次の配列が得られます。

[3, 6, 9, 12, 15, 18, 21, 24, 27, 30]

filter

クロージャーで与える条件に合う項目だけ取りだして配列を返します。このクロージャはboleanを返す関数を用意します。

var filterNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .filter({(a: Int) -> Bool in return a % 2 == 0})

配列の項目を2で割って余りが0、2で割り切れる項目(偶数)のみ取りだした配列を返します。このクロージャもmap同様に省略が可能です。

  • 戻り値の型は省略できます。
var filterNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .filter({(a: Int) in return a % 2 == 0})
  • 同様にreturnも省略できます。
var filterNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .filter({(a: Int) in a % 2 == 0})
  • そして最終的にTrailing Closureに持っていけます。
var filterNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .filter {($0 % 2) == 0}

このフィルターで取り出された配列は次のものです。

[2, 4, 6, 8, 10]

reduce

配列の項目を減らすという意味で、初期値と条件をクロージャとして与えることで配列項目を減らします。

var reduceNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .reduce(0, {(a: Int, b: Int) -> Int in return a + b})

reduceに渡す引数は2つ。最初の「0」は初期値で、2つ目はクロージャー(このクロージャも2つの引数をとります)で、初期値「0」がクロージャの最初の引数「a」に渡されます。引数「b」には配列の最初の値(1)が渡され、「a + b」の処理をして再び、「a」に渡します、「b」に配列の2番目の項目(2)を加え、同様に配列最後の項目まで計算が続いて。最後に55が得られます。これも同様に省略形があります。

  • 戻り値の型の省略はできます。
var reduceNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .reduce(0, {(a: Int, b: Int) in return a + b})
  • 当然のようにreturnも省略できます。
var reduceNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .reduce(0, {(a: Int, b: Int) in a + b})
  • 型宣言も省略できます。
var reduceNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .reduce(0, {(a, b) in a + b})
  • Trailing Closure」に変更できます。
var reduceNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .reduce(0) {(a, b) in a + b}
  • 当たり前のように引数も省力して「$0」を使います。
var reduceNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        .reduce(0) {$0 + $1}

すべての場合次の結果が得られます。

55

Swift 4からreduceの扱いが便利になるという情報を見つけたので「Swift 4の新しいreduceが素晴らしいので紹介する - Qiita」を読み感動しました。私が感動したのは初心者なりに基本的なところです。

「reduce」は配列の項目を減らすメソッドなんで、どんな使い道があるのかサッパリ分からなかったのですが、この記事を読んで、配列(Array)や辞書(Dictionary)を生成するメソッドとして使えるってところに感動しちゃいました(^_^;)

初期値に空のArrayを与えてやると新しい配列をつくることができるんです!なんとDictionaryも生成できるとは超感動です。

まず、Arrayを作り出す次のコード

var reduceNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

var newNumbers = 
    reduceNumbers.reduce([]) { $0 + [($0.last ?? 0) + $1] }

// [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]が得られます

初期値に空のArrayを与えてやるんです。Arrayの持っているメソッド「last」を使って、最後の項目を取り出しますがnilの場合があるので「??」を使ってアンラップします。そして元のArrayの最初の項目でArrayを作ります。お見事です!しかし、あまり利用する機会はないような気もします(^_^;)

それよりもDictionaryの生成は覚えておいて損はないでしょう。現状での問題点を3つ挙げていました。書きづらい 可読性が低いパフォーマンスが悪いですが、私としたらできるんだってことがおどろきでした!

まずコードを理解するのに用意しなければならないのが、UserクラスインスタンスのArrayです。このUserクラスにはInt型のプロパティ「id」が必要です。

var users = [john, tom, carl, henrry, mack, kenny]

var idUsers = users.reduce([Int: User]()) {
    var result: [Int: User] = $0
    result[$1.id] = $1
    return result
}

従来のreduceで渡すパラメータは値を変更することができないんで、Dictironary型のresultという空の変数を用意します。$0も空のDictrinaryなんでもどかしい処理ですよね。これがパフォーマンス低下を導いています。そして、最初の項目($1)の"john"の持っている「id」プロパティをKey値にして"john"自身をValue値としてDictionaryを生成します。そしてそれをresultとして$0に戻す作業です。

確かに理解しがたくパフォーマンスの問題もあるのですが、reduceを使ってこういう作業ができることに感銘を受けました。そして、Swift 4からこのreduceのパラメータの扱いが拡張されたようです。

$0に「inout」がつくのでそのまま変更して戻すことができます。

// 下記の記述は現状ではできません。
// swift 4から実装です。
users.reduce(into: [Int: User]()) { $0[$1.id] = $1 }

直感的にも分かりやすいし、こういう処理は覚えておいて損はないでしょう!

メソッドの連結

そして最後にこれらのメソッドを連結して連続に処理をこなすことができます。

var chainNumbers = 
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      .map({(a: Int) in a * 3})
         .filter({(a: Int) in a%2 == 0})
            .reduce(0, {(a: Int, b: Int) in a + b})

これで次の結果が得られますが、説明はいりませんね。

90

joined(separator:)

// joined(separator:)

func joined<Separator>(separator: Separator) -> 
  JoinedSequence<Self> where Separator : Sequence, 
  Separator.Element == Self.Element.Element 

こういうジェネリックな表現のメソッドの表現法にもだいぶ慣れました。このメソッドはArray型のSeparator(ジェネリック表現)がArrayで与えられます。つまり、Array型で与えられた場合、元(Self)のArrayのElementのArrayでないといけません。SeparatorのElementの型が、SelfのElement(Array)のElementの型と一致する条件のときだけ結合します。何のことやらよく分かりませんね。でも、例を挙げれば理解しやすいでしょう。

let array = [[1, 2, 3], [23, 33, 45], [123, 456, 234]]
let joined = array.joined(separator: [0, 0])

// Array(joined) = [1, 2, 3, 0, 0, 23, 33, 45, 0, 0, 123, 456, 234]

こういうスタンダードライブラリを使えるようにならないといけませんね。

便利なArrayの標準メソッドの勉強をしました。

*1:2017年5月20日:Swift 4で拡張されるreduceの説明追加、2017年5月19日:mapの省略フォームを追加、2016年11月20日:改訂