Edited at
RxSwiftDay 15

bindToとかaddDisposableToとか鬱陶しくない?演算子に置き換えちゃえ!

More than 1 year has passed since last update.


はじめに

RxSwift Advent Calendar が空いていたので、ちょっと思いついたことを記事にしてみました。

これはバッドノウハウかもしれません。でも嬉しいって人は多いはず!


やりたいこと

RxSwift を使っていると、以下のようなコードがよく出てきます。

presenter.buttonTitle.bindTo(button.rx.title)

.addDisposableTo(disposeBag)
presenter.labelText.bindTo(label.rx.text)
.addDisposableTo(disposeBag)

この bindTo とか、特に addDisposableTo とかを演算子に置き換えて

presenter.buttonTitle |> button.rx.title +> disposeBag

presenter.labelText |> label.rx.text +> disposeBag

って書けたら楽だし見やすくない?という思いつきです。

これはちょっとやり過ぎって言うなら、せめて addDisposableTo をまとめて書けるようにしたい!

disposeBag.addDisposables(

presenter.buttonTitle.bindTo(button.rx.title),
presenter.labelText.bindTo(label.rx.text)
)


実装

手元の環境がまだ Xcode 7.3 で Swift 3 が使えません。なので Swift 2.2 で動作確認しています。以下をどこかに書いておくだけです。

infix operator |> {associativity left precedence 140}

infix operator +> {associativity left precedence 140}

func |> <L: ObservableType, R: ObserverType where R.E == L.E>(lhs: L, rhs: R) -> Disposable {
return lhs.bindTo(rhs)
}

func |> <L: ObservableType>(lhs: L, rhs: Variable<L.E>) -> Disposable {
return lhs.bindTo(rhs)
}

func +> (lhs: Disposable, rhs: DisposeBag) {
lhs.addDisposableTo(rhs)
}

演算子は存在しないものを新たに定義して使っています。既に存在するものは「期待する動作」があるわけなので、それとは違う意味を持つ演算子は新たなものを用意した方がいいと思います。演算子の優先順位は全く何も考えずに適当に140を設定しています。これは Swift演算子まとめ によると + や - と同じになるみたいです。

Swift 3.0 では演算子の定義は型に所属させるので、拡張する場合は extension の中に書くみたいです。またジェネリクスの where を後ろに持っていけます。

infix operator |>: AdditionPrecedence

infix operator +>: AdditionPrecedence

extension ObservableType {
static func |> <L: ObservableType, R: ObserverType>(lhs: L, rhs: R) -> Disposable
where R.E == L.E {
return lhs.bindTo(rhs)
}

static func |> <L: ObservableType, R: ObserverType>(lhs: L, rhs: R) -> Disposable
where R.E == L.E? {
return lhs.bindTo(rhs)
}

static func |> <L: ObservableType>(lhs: L, rhs: Variable<L.E>) -> Disposable {
return lhs.bindTo(rhs)
}
}

extension Disposable {
static func +> (lhs: Disposable, rhs: DisposeBag) {
lhs.addDisposableTo(rhs)
}
}

演算子の優先順位は + や - と同じになっていますが、これも何も考えずに Swift 公式の解説 のコードを真似しただけです。コンパイルが通るかすら確認していないので、間違いがあるかもしれません。


利点・欠点

bindTo や addDisposableTo は(RxSwift 知らないと厳しいかもしれませんが)初見で何をやっているか言葉から推測できますし、RxSwiftを知っている人には理解できます。しかし記号にしてしまうと、記号の意味を知っている人にしか分かりません。

また Swift には記述時の文字数の少なさよりも言葉が明確なことを好む文化があります。なので最初にバッドノウハウかもしれませんと書きました。

しかしソースコードを読み書きをするチームメンバー全員がわかっているのなら、むしろ区切りが明確になり大事な部分だけが目立つので、読みやすく分かりやすいと感じるのではないでしょうか?


addDisposables版

さすがに演算子にするのはちょっとやりすぎだと思うなぁという人には、以下の拡張が便利かと思います。

extension DisposeBag {

func addDisposables(disposables: Disposable...) {
for disposable in disposables {
disposable.addDisposableTo(self)
}
}
}


Driverのdriveも

演算子化するなら Driver の drive にも同じように演算子を用意したいですね。bindTo とは別の演算子にすることで、Presenter に対して Driver で提供することを強制できます。Swift 2.2, RxSwift 2.6 の場合は以下でコンパイルが通りました。

infix operator *> {associativity left precedence 140}

func *> <L: DriverConvertibleType, R: ObserverType where R.E == L.E>(lhs: L, rhs: R) -> Disposable {
return lhs.drive(rhs)
}

func *> <L: DriverConvertibleType>(lhs: L, rhs: Variable<L.E>) -> Disposable {
return lhs.drive(rhs)
}

Swift 3.0, RxSwift 3.0 の場合は、以下のようになりそうですが、コンパイルの確認すらできていません。

infix operator *>: AdditionPrecedence

extension SharedSequenceConvertibleType where SharingStrategy == DriverSharingStrategy {
static func *> <L: SharedSequenceConvertibleType, R: ObserverType>(lhs: L, rhs: R) -> Disposable
where L.SharingStrategy == L.DriverSharingStrategy, R.E == L.E {
return lhs.drive(rhs)
}

static func *> <L: SharedSequenceConvertibleType, R: ObserverType>(lhs: L, rhs: R) -> Disposable
where L.SharingStrategy == L.DriverSharingStrategy, R.E == L.E? {
return lhs.drive(rhs)
}

static func *> <L: SharedSequenceConvertibleType>(lhs: L, rhs: Variable<L.E>) -> Disposable {
where L.SharingStrategy == L.DriverSharingStrategy {
return lhs.drive(rhs)
}
}


演算子はお好きなものを

演算子はチームのみんなが読みやすく覚えやすいものを選べばいいです。今回思いつきで使った演算子は、


  • bindTo は F#, Elixir などのバイプライン演算子っぽい感じで

  • addDisposableTo は add が付いてるので + を使って表現

  • drive は * がハンドルとか船の舵を思わせるので

という理由で選んでみました。