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を使ってやると、こんなふうに書けます。
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()のカッコが書かれてなかったですね。
これはトレイリングクロージャ(接尾クロージャとも)と言われる記法で、
関数の最後の引数がクロージャの場合は、()の外に書ける記法です。
冗長な書き方を見てもわかるとおり、クロージャが引数だと、
{}の外に()を包まないといけないので、可読性悪くなりますね。
2.型推論による省略
クロージャは引数と返り値を持ちますが、
人が書いてるコード見るとあまり明示的に書いてるケースの方が少ない気がします。
そもそもmapの関数定義は、下記のようになっています。
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
Array型はmapというメソッドを持っていて、
Array型の各要素をクロージャに内包される形で受け取って、
なんかしらの処理をして、最終的にArray型にして返してやる、というのがmapです。
クロージャの引数・返り値に対しては、型推論が効くので、省略できます。
今回のケースでは、String型の一次元配列を要素として受け取って、
Int型の一次元配列として吐き出し、最終的に[[Int]]の二次元配列になります。
もしプログラム上、コンパイラが型を推測できないような処理を書いちゃうと、コンパイルエラーになります。
3.簡略引数名
冗長な記法だと、配列の各要素をelementという引数名を使って受け取ってました。
クロージャは簡略引数名というのが使えます。
$0で配列の各要素を示します。
これを使うと、上記elementという引数名は省略できますね。
引数名を省略する場合、inキーワードも一緒に省略する習わしみたいです。
4.暗黙的なreturn(Implicit Returns)
だいぶ省略できましたね。
最後にreturnも消しちゃいましょう。
クロージャは文が一文しかない場合は、その計算結果を出力としてreturnします。
これを暗黙的なreturn(Implicit Returns)と言うそうです。
ここまで紐解いていくと、最初の省略形がいかに多くの情報を省略した、簡潔な書き方なのかがわかりますね。
sortとfileter
思いの外mapの説明だけで時間を食ったので、
当初sortとかfilterとかも調べた結果を書こうと思ってたのですが、
ちょっとしんどいので、参考サイトを見てください。
sortやfilterだとクロージャを使って条件を指定できます。
関数型っぽい書き方についての個人的感想
話題になってるHaskellをかける少女とかでも、
関数型プログラミングっぽい記法が紹介されていますが、
確かに小難しい処理を簡潔に書けるので、嬉しい気はします。
ただそれはソースコード的な綺麗さだけの話らしく、
(言語にもよるでしょうが)
パフォーマンスがよくなるわけではないようです。
map/sort/filterあたりは使えなくても、自分で制御文書けば代用できるのですが、
折角Swiftで書くんであれば、使いこなせるようになりたいですね。