LoginSignup
1
2

More than 3 years have passed since last update.

Swift: 配列だってチェーンしたい

Last updated at Posted at 2020-12-29

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で実装されているが、あたかも新たな言語仕様が追加されたかのようにも見える。

1
2
0

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
1
2