[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
Scala 2.13からscala.util.chainingパッケージが追加されました。
これは、Scalaで手軽にメソッドチェーンを構築するためのパッケージです。
2.12以前のScalaでも限定的な状況下ではメソッドチェーンをすることができましたが、 以前よりさらに手軽にメソッドチェーンをすることができるようになりました。
この記事ではscala.util.chainingを利用して、メソッドチェーンを作って処理を行う方法を解説します。
メソッドチェーンとは「処理の連鎖」
メソッドチェーンとは、一連の処理を連鎖的に繋げて書くことです。
メソッドがまるで鎖のようにつながっているさまを指して「メソッドチェーン」と呼んでいます。
実装上は、メソッドの処理の結果としてオブジェクト自身を返すことによって実現されることが多いです。
例えばScalaにおいても、OptionのmapメソッドはOptionを返すため、
Optionのもつメソッドを何個も繋げてメソッドチェーンをすることができます。
Option(1) .map(_ + 2) .map(_ * 3) .foreach(println)
ScalaではOptionなどの値を包みこむような構造の型を使っている場合にメソッドチェーンができることが多かったですが、
scala.util.chainingによって、そうでない型のオブジェクトに対してもメソッドチェーンができるようになりました。
Optionなどの、値を包みこむような構造のクラスであればmapやforeachメソッドが備わっていることが多いので、
すでにオブジェクトがそれらのクラスである場合はそれを使えばOKです。
他の言語におけるメソッドチェーン
他の言語においてメソッドチェーンを活用している例としては、特にjQueryが有名です。
メソッドチェーンの使い方
scala.util.chaining._をインポートするだけ
Scalaでメソッドチェーンをするには、scala.util.chainingパッケージの中身をインポートします。
import util.chaining.*
これにより、スコープ内のあらゆるオブジェクトがpipeやtapメソッドを持つようになります。
その理由は暗黙の型変換によるのですが、後ほど詳述します。
tapメソッドは副作用を発生させるために使う
副作用が発生するような処理の場合はtapメソッドを使います。
1.tap(n => println(s"tapped $n")) .pipe(_ * 3) .tap(n => println(s"tapped $n"))
結果は以下のようになります。
tapped 1 tapped 3
pipeメソッドは処理結果をその後に伝えるために使う
結果の値を渡す場合は、pipeメソッドを使います。
1.tap(n => println(s"piped $n")) .pipe(_ + 2) .tap(n => println(s"piped $n")) .pipe(_ * 3) .tap(n => println(s"piped $n"))
結果は以下のようになります。
piped 1 piped 3 piped 9
メソッドチェーンのメリット・デメリット
scala.util.chainingパッケージは必ずしも使わなければならないものではありません。
メソッドチェーンはひとつのコーディングスタイルであり、 したがってこのパッケージは必要に応じて使用するものといえます。
以下のメリット・デメリットを踏まえて、使うかどうかを判断しましょう。
メソッドチェーンのメリットは、処理の流れを明確にできること
メソッドチェーンは、その書き方の特性上、処理の流れ、順番が明確になります。 非同期処理を挟まない限り、前から後ろに向けて順番に処理されることが期待できます。
処理がメソッドチェーンの各スコープの中で完結するため、一時変数を定義したり命名する手間が省けます。 するにしても、スコープを限定できるので不用意に変数へアクセスすることを防ぐことができます。
メソッドチェーンのデメリット1:複雑な流れや分岐を表現できない。
メソッドチェーンは処理の流れを明確にしやすいのがメリットでした。
ただし、途中で分岐するような処理は表現できません。
そのような場合はメソッドチェーンを使うのを諦めるとよいでしょう。
メソッドチェーンのデメリット2:非同期処理に向かない。
あるいは、前から順番に処理されるように見えることが誤解を生んでしまう場面もあります。
メソッドチェーン内で非同期処理を実行していて、実際にはどの順番で行われるかが定かでない場合には、 読み手が誤解してしまい、あたかも前から順番に処理されると思いこんでしまう可能性があります。
そのような場合には、メソッドチェーンを使うことは避けましょう。
メソッドチェーンのデメリット3:コンパイル時間に影響を及ぼす「可能性」
これはメソッドチェーンそのもの問題ではなく、Scalaにおける実装上の問題です。
scala.util.chainingパッケージではメソッドチェーンを実現するための実装に暗黙の型変換を使用しているので、
原理的にはコンパイル時間に悪影響を及ぼす可能性があります。
あくまで「可能性」というのがポイントです。
限定的に用いるのみであれば、全く問題はないでしょう。
大量に使った場合において許容範囲内におさまるかどうかは、プロジェクトによります。
scala.util.chainingを頻用する場合にはこの点について留意しておき、コンパイル時間が気になってきた際には必要に応じて計測するなどして随時再検討しましょう。
その他のトピック
util.chaining._をインポートするとtap/pipeメソッドが使えるようになるのは「暗黙の型変換」が働くから
なぜ scala.util.chainingの中身をインポートするとtap/pipeメソッドを使えるようになるのでしょうか。
その理由は「暗黙の型変換」にあります。
暗黙の型変換については別の記事にて詳しく解説しています。
まず、scala.util.chainingには暗黙の型変換を引き起こすscalaUtilChainingOpsというメソッドが含まれています。
scala.util.chainingこれがそのメソッドです。
implicit final def scalaUtilChainingOps[A](a: A): ChainingOps[A]
このメソッドはAという型を受け取って、ChainingOps[A]という型に変換するメソッドです。
scala.util.ChainingOpsChainingOps型について見てみると、このChainingOpsがtapやpipeメソッドを持っていたことがわかります。
前述の例で、単なるIntであるはずの1がtapやpipeメソッドを持っていたのは、
コンパイラがIntからChainingOps[Int]へとこっそり型を変換してくれていたからなのです。
Dotty (Scala 3)ではバージョン0.18から使える
Dotty (Scala 3)では、バージョン0.18からscala.util.chainingパッケージを使うことができます。
Dotty 0.17までの標準ライブラリはScala 2.12のものだったため使用できませんでしたが、 Dotty0.18からはScala 2.13の標準ライブラリへと切り替えられたため、メソッドチェーンを使用できるようになりました。
コンパイラオプション-Yimportsを使ってインポート不要で使えるようにする
コンパイラオプション-Yimportsにてscala.util.chainingを追加指定すると、
コードにおいて明示的にscala.util.chainingパッケージをインポートすることなく、
tapやpipeメソッドを使うことができるようになります。
前述のとおり、scala.util.chainingパッケージの実装上、暗黙の型変換を使用するので、
原理的にはコンパイル時間に悪影響を及ぼす可能性があるという点に留意しておきましょう。