LoginSignup
9
8

More than 3 years have passed since last update.

Swiftっぽい配列の処理をするためのクロージャの使い方を学んだ(map, sort, filter)

Last updated at Posted at 2019-01-20

Swiftを真面目に勉強し始めて、まだ一ヶ月たってないくらいなんですが、
やはりクロージャに苦手意識があります。

クロージャって結局何なのか
以前こんなQiita記事も書いたんですが、この段階では教科書的な定義はわかったものの、
じゃあ具体的に使うケースは……? というのはよくわからないままでした。

そんなとき、配列扱ってるときに、要素の追加とかをいちいちループさせてたんですが、
mapってやつを使って、引数にクロージャ与えると、たった一行で実装できる、というのを知り、
えっクロージャ強いじゃん、となりまして、勉強してみました。

Swiftっぽくない書き方

mapを使わないで書くと、こんな感じです。

標準入力を二次元配列に
//1 3 1
//3 2 2
//2 3 5
//3 4 4
//1 6 6
//こんな標準入力が与えられるので、[[1,3,1],[3,2,2],...]みたいな感じで初期化したい
let numberOfInputLines = 5
var input_lines = [String]() //標準入力をとりあえず受け取る
var outArray: [[Int]] = [[Int]]() //変換先の二次元配列

for i in 0..<numberOfInputLines {
    input_lines.append(readLine()!)
    let splits: [String] = input_lines[i].components(separatedBy: " ") 
    outArray.append([Int(splits[0])!, Int(splits[1])! , Int(splits[2])!])
}
print(outArray) //[[1, 3, 1], [3, 2, 2], [2, 3, 5], [3, 4, 4], [1, 6, 6]]

これでもやりたいことはできているものの、

  • やりたいことに対してコードが長い
  • .append()の繰り返しがなんかダサい
  • outArrayは初期化したあとは内容変更しないのに、.append()で要素追加したいがためにvarで指定している

などの問題があります。

Swiftっぽい書き方

.mapを使ってやると、こんなふうに書けます。

標準入力を二次元配列に(map版)
let numberOfInputLines = 5
var input_lines = [String]()
var splits =  [[String]]() //for文の外に

for i in 0..<numberOfInputLines {
    input_lines.append(readLine()!)
    splits.append(input_lines[i].components(separatedBy: " ")) //[["1", "3", "1"], ["3", "2", "2"], ["2", "3", "5"], ["3", "4", "4"], ["1", "6", "6"]]
}    

let outArray = splits.map { [Int($0[0])!, Int($0[1])!, Int($0[2])!] }
/*
2019/1/21 追記
splitsという配列をつくっていますが、mapを二重で使うと不要です。
ただちょっと説明しづらいですね。。。
let outArray = input_lines.map { $0.components(separatedBy: " ").map { Int($0)! } }
*/

print(outArray) //[[1, 3, 1], [3, 2, 2], [2, 3, 5], [3, 4, 4], [1, 6, 6]]

すみません書いてて思ったんですが、あんまりいいサンプルじゃなかったですね……
paizaのA問題解いてるときにこういう処理が必要だったんです。

.mapを解読する

よくある書き方
let outArray = splits.map { [Int($0[0])!, Int($0[1])!, Int($0[2])!] }

さてmapの書き方ですが、皆さんは意味がわかりますか?
僕は全然わかりませんでした。
なぜわかんないのか調べていくと、いろんなものが省略されているからだとわかりました。

敢えて一切省略せず、冗長に書くと、mapは下記のようにかけます。

敢えてクロージャを一切省略値使わずに冗長に書く
let outArray = splits.map({ (element: [String]) -> [Int] in 
    return [Int(element[0])!, Int(element[1])!, Int(element[2])!]
})

全然雰囲気変わりましたね。
でもやってることは同じです。
いちいちこんな書き方しなければいけないんだったら、mapなんて使わないで、
ループでぐるぐるappendさせてもそんな変わんないような気もしますね。

では何が省略されていたのか、ひとつずつ見ていきましょう。

1.トレイリングクロージャ

まず最初の書き方では、
.mapは関数なのに、.map()のカッコが書かれてなかったですね。

70DE69B5-321E-42FB-8C2E-2FEFE19727AF.png

これはトレイリングクロージャ(接尾クロージャとも)と言われる記法で、
関数の最後の引数がクロージャの場合は、()の外に書ける記法です。
冗長な書き方を見てもわかるとおり、クロージャが引数だと、
{}の外に()を包まないといけないので、可読性悪くなりますね。

2.型推論による省略

クロージャは引数と返り値を持ちますが、
人が書いてるコード見るとあまり明示的に書いてるケースの方が少ない気がします。
7D332E82-0246-4973-84DC-4FB1E499381E.png

そもそもmapの関数定義は、下記のようになっています。

mapの関数定義
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

Array型はmapというメソッドを持っていて、
Array型の各要素をクロージャに内包される形で受け取って、
なんかしらの処理をして、最終的にArray型にして返してやる、というのがmapです。

image.png
イメージ図としてはこんな処理です。

クロージャの引数・返り値に対しては、型推論が効くので、省略できます。
今回のケースでは、String型の一次元配列を要素として受け取って、
Int型の一次元配列として吐き出し、最終的に[[Int]]の二次元配列になります。
もしプログラム上、コンパイラが型を推測できないような処理を書いちゃうと、コンパイルエラーになります。

3.簡略引数名

冗長な記法だと、配列の各要素をelementという引数名を使って受け取ってました。

17DB30F0-1137-45FF-A575-32E50DDC9E83.png

クロージャは簡略引数名というのが使えます。
$0で配列の各要素を示します。
これを使うと、上記elementという引数名は省略できますね。
引数名を省略する場合、inキーワードも一緒に省略する習わしみたいです。

4.暗黙的なreturn(Implicit Returns)

だいぶ省略できましたね。
最後にreturnも消しちゃいましょう。
クロージャは文が一文しかない場合は、その計算結果を出力としてreturnします。
これを暗黙的なreturn(Implicit Returns)と言うそうです。

ここまで紐解いていくと、最初の省略形がいかに多くの情報を省略した、簡潔な書き方なのかがわかりますね。

sortとfileter

思いの外mapの説明だけで時間を食ったので、
当初sortとかfilterとかも調べた結果を書こうと思ってたのですが、
ちょっとしんどいので、参考サイトを見てください。
sortやfilterだとクロージャを使って条件を指定できます。

関数型っぽい書き方についての個人的感想

話題になってるHaskellをかける少女とかでも、
関数型プログラミングっぽい記法が紹介されていますが、
確かに小難しい処理を簡潔に書けるので、嬉しい気はします。

ただそれはソースコード的な綺麗さだけの話らしく、
(言語にもよるでしょうが)
パフォーマンスがよくなるわけではないようです。

map/sort/filterあたりは使えなくても、自分で制御文書けば代用できるのですが、
折角Swiftで書くんであれば、使いこなせるようになりたいですね。

参考サイト

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

9
8
1

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
9
8