このメソッドには問題がある
extension Array {
func reversedMap<T>(_ transform: (Element) -> T) -> [T] {
map(transform).reversed()
}
}
じょ
一見普通に見えるメソッドですが、何が問題なのでしょう
Array#reversed()
Array
には要素の内容を逆順にするreversed()
というメソッドがあります。
このメソッドはよくできていて、実際に要素が逆順になったArrayを生成せずにそのようにふるまうReversedCollection<Base>
という型のインスタンスを返します。
このメソッドはArray
ではなくBidirectionalCollection
プロトコル上にextensionとして実装されています。
func reversed() -> ReversedCollection<Self>
BidirectionalCollection
は要素を逆順に走査することができるコレクションですので、このreversed()
を実装するのにちょうどよいプロトコルといえるでしょう。
Sequence#reversed()
感の良い方ならすでにお気づきだとは思いますが、初めに提示したreversedMap
メソッドの戻り値はArray
となっており、BidirectionalCollection#reversed()
に適合していません。
ではこのArray
を戻すreversed()
メソッドはどこから来たのでしょう。
それはSequence
プロトコル上にextensionとして実装されています。
func reversed() -> Array<Element>
Sequence
プロトコルは要素が順番に取り出せるということのみが規定されたプロトコルです。
PartialRangeFrom
(5... のようなやつ)のように終端がないSequence
も実装可能です。
初めに提示したreversedMap
メソッドはこちらのreversed()
メソッドが使われているということです。
2つの相違
ではSequence#reversed()
を使うことの何が問題なのでしょう。
ReversedCollection<Base>
はBase
がBidirectionalCollection
であることを利用し、新たなCollection
を生成することなく逆順に要素を取り出すことができます。
ところがSequence
から逆順のArray
を生成するためには、すべての要素を取り出した上で逆順になったArray
を生成する必要があるのです。
計算量でいえばBidirectionalCollection#reversed()
はO(1)で、Sequence#reversed()
はO(N)です。
もちろんこの違いはとてつもなく大きいです。
じゃあどうすればいいの?
方法は2通りあります。
まずメソッドの順番を変える方法。
こちらの方がわかりやすいです。
extension Array {
func reversedMap<T>(_ transform: (Element) -> T) -> [T] {
reversed().map(transform)
}
}
reversed()
メソッドを先に実行します。こうするとコンパイラはより適用範囲がより狭いBidirectionalCollection#reversed()
を必ず選択します。
二つ目は戻り値の型を変える方法。
extension Array {
func reversedMap<T>(_ transform: (Element) -> T) -> ReversedCollection<Self> {
map(transform).reversed()
}
}
こうすることで、BidirectionalCollection#reversed()
に固定されます。
ただし、ちょっと見た目がうるさいです。
まとめ?
手元でてきとーにテストしてみた結果、BidirectionalCollection#reversed()
はSequence#reversed()
より1割程度高速になります。
戻されたSequence
を使わないと意味がないのでテストは
measure {
s = array.reversedMap { i in i * 2 }.reduce(0, +)
}
で行いました。
すごくてきとーにやってますので興味がある方はお手元でお確かめください。