Optionalだけ特別扱いずるくないか
SwiftでOptional Chainingというものがあったが、言語仕様でそんな飛び道具をサポートするなら配列やそのほかの似たような(モナモナしてる)やつらもサポートしてあげて欲しい。
今更だとは思うが、オプショナルチェーンとはご存知の通り
struct Person {
let name: String
let age: Int
let friends: [Person]
}
let me: Person = .init(name: "Freddie", age: 30, friends: [])
let someone = me.friends.first?.friends.first?.friends.first // <- Person?
のように?を使って連続的にプロパティアクセスできる。
しかし問題は、なぜこの構文を配列やResult型でもサポートしてくれないのか。
オプショナルと配列をなぜ対等に扱ってくれと引き合いに出すのかと言えば、非常に長くなるので「これらは共通してモナドだ」という説明で割愛しておく。乱暴に言えば、オプショナルも配列もmapやflatMapってメンバー関数持ってて何だか似たもの同士だろ?ってところだ。
戯(や)りたいこと
どういうことがしたいかというと、配列においてこんな夢のようなかっこいい書き方をしたい。
let people: [Person] = [.init(name: "me", age: 32, friends: [.init(name: "Tom", age: 40, friends: [])]), .init(name: "you", age: 28, friends: [.init(name: "Nancy", age: 10, friends: []), .init(name: "Freddie", age: 20, friends: [])])]
let totalAge = people[].age.reduce(0, +) // 60
let ourFriends = people[].friends[].name // ["Tom", "Nancy", "Freddie"]
名付けるなれば、Array Chaining(アレーチェイニング)ってところか。
people[].age
この通り書けるなら、短くて美しくないだろうか?
よ〜くある
people.map { $0.age }
とか、せいぜい
people.map(\.age)
みたいなマサラタウンレベルのコードにはサヨナラバイバイしよう。
それでは実装していこう
今回は配列を扱うのでメソッドチェーンを使うよという合図は?でなく[]でいこうと思う。
public extension Collection {
subscript() -> ArrayMemberLookupReference<Self> {
.init(source: self)
}
}
public extension Collection where Element: Collection {
subscript() -> ArrayMemberLookupReference<[Element.Element]> {
.init(source: lazy.flatMap { $0 })
}
}
@dynamicMemberLookup
public struct ArrayMemberLookupReference<C: Collection> {
fileprivate let source: C
}
extension ArrayMemberLookupReference {
subscript<Property>(dynamicMember keyPath: KeyPath<C.Element, Property>) -> [Property] {
source.map { t in t[keyPath: keyPath] }
}
}
使ってみるぞ
さて、こんなものが果たしてほんとに使えるのかと思われるかも知れない。
今一度サンプルコードを示してみよう。
struct Person {
let name: String
let age: Int
let friends: [Person]
}
let people: [Person] = [.init(name: "me", age: 32, friends: [.init(name: "Tom", age: 40, friends: [])]), .init(name: "you", age: 28, friends: [.init(name: "Nancy", age: 10, friends: []), .init(name: "Freddie", age: 20, friends: [])])]
let totalAge = people[].age.reduce(0, +) // 60
let ourFriends = people[].friends[].name // ["Tom", "Nancy", "Freddie"]
是非お試しあれ。
実際に動くかと疑われる方はPlaygroundで実行してみて欲しい。
実際に現行のSwiftで実装されているが、あたかも新たな言語仕様が追加されたかのようにも見える。