はじめに
Swift でプログラミングをしていても、UI 周り部分の実装がメインとなり、Generics や Higher Order Function をあまり活用できていないように思います。
他の言語扱う中でも、必ずと言っていいほど、Generics や Higher Order Function は出てきますし、カッコ良いコードを書いたほうがモチベーションが高く維持できるので、今回は Swift 言語を使ってこれらを復習していきます。
名前リストを挨拶リストに変換
let nameList: [String] = ["Yamada", "Tanaka", "Nakano"]
名前リストから、 敬称
と挨拶
をつけたリストを作り出すには、map
を使ってシンプルに書くと、
let morningMrList = nameList.map { "Mr. \($0) Good morning!" }
のようになります。map
を使っているので、十分スッキリ書けていると思います!
ただ、敬称を Ms.
に変更したい、 挨拶を、Good evening!
に変更したい
let morningMsList = nameList.map { "Ms. \($0) Good morning!" }
let eveningMrList = nameList.map { "Mr. \($0) Good evening!" }
のようにベタ書きため再利用性はないものであることに気づきます。
この問題を解決するために、最初に考えられるのは、map 処理の中で 敬称
と 挨拶
を一度に文字列変換していますが、これらを分離するアプローチです。
let morningMrList2 = nameList.map { "Mr. \($0)" }.map {"\($0) Good morning!"}
処理が別れたので再利用性が高くなりそうな気がします。
次に、名前リスト毎に文字列加工処理(敬称+挨拶)を map で変換処理を書くのは少し無駄な気がするので、名前リストと文字列加工処理をパラメータとして受け取る関数を用意します。関数をパラメータとして受け取る高階関数となります。← 少し玄人感がでてきました。
func _map<String>(_ list: [String], f: (String) -> String) -> [String] {
return list.map { f($0) }
}
let morningMrList3 = _map(nameList) { "Mr. \($0) Good morning!" }
残念ならが、これも、敬称と挨拶が合体しているので、再利用性は低いです。
敬称追加用の関数+挨拶追加用の関数を 2 つ受け取る形で改善させる方法も考えられますが、以下でスマートな解決策を紹介します。
敬称、挨拶の分離+文字列加工の関数化
- 敬称、挨拶の分離
- 文字列加工の関数化
この 2 つが実現えきれば、名前リストから挨拶リストを生成するのに、毎回ベタ書きしなくてよくなります。
上の map では、戻りの型が配列でしたが、戻り値を関数に書き換えます。
この map 関数は、戻り値の型に束縛されないので Generics で書くことができます。
func map<A, B>(_ f: @escaping (A) -> B) -> ([A]) -> [B] {
return { $0.map(f) }
}
すごく汎用的なコードになりました。 戻り値は、引数に任意の型の配列(A)を受け取り、任意の型の配列(B)を返す関数となります。
アロー( ->
) が連続すると、とたんに可読性が落ちる気がしますが、代わり玄人感はすごく増しますね。
この map を使うと以下のような関数が用意できます
// 名前に、敬称のMrを追加する関数
let mrF: ([String]) -> [String] = map{ "Mr. \($0)" }
// 名前に、挨拶を追加する関数
let morningF: ([String]) -> [String] = map{ "\($0) Good morning!" }
関数なので、nameList, nameList2 のように別のリストでも、引数を変えるだけ。
let morningMrList4 = morningF(mrF(nameList))
let morningMrList5 = morningF(mrF(name2List))
他の挨拶の関数も同様に、書けます。
let msF: ([String]) -> [String] = map{ "Ms. \($0)" }
let afternoonF: ([String]) -> [String] = map{ "\($0) Good afternoon!" }
let eveningF: ([String]) -> [String] = map{ "\($0) Good evening!" }
これらを組み合わせれば名前から挨拶リストを作り出せるので、再利用可能です。
関数ネストを改善したい
上の例ですが、関数がネストしているので少し見にくい気がします。
morningF(mrF(nameList))
これを解決するには、関数を合成する関数を用意すれば良いでしょう。本格的な高階関数ですね。
func compose<A, B, C>(_ f: @escaping((A) -> B), _ g: @escaping((B) -> C)) -> (A) -> C {
return { a in g(f(a)) }
}
2 つの関数を引数を受け取ります。受け取った、2 つの関数を合成して関数を返却しています。型に縛られない関数なので、Generics で書くことが出来て再利用しやすいです。
これを使うと、
let msMorningF = compose(msF, morningF)
使う側も関数がネストしないので済むので、可読性があがります。
let morningMsList2 = msMorningF(name2List)
まとめ
毎回、同じ処理を書く場合は、高階関数(特に、戻り値を関数)にすることを検討すると再利用性が高いコードになることが理解できました。Swift の高階関数の記事は、 map
flatMap
reduce
などの使い方を説明に閉じた記事が多いですが、自身で高階関数を用意して活用することが本当の活用な気がします。
そして、Generics +高階関数の組み合わせは非常に強力ですね。
この記事にあたり、Point Free で勉強させてもらいましたが、 海外サイトは良質な記事が多いので、英語頑張りたいです。