1. はじめに
Scalaを用いたアプリケーションコードでは、型パラメータ
を用いたくなることがあるかと思いますが
その中でも特に、多く(例えば5つ)の型引数が登場するようなクラスを実装したい
というモチベーションが湧くことがあり、この記事ではその実装の工夫について紹介してみようと思います。
具体的なコードを書いて例を示すと、以下のような
抽象クラスの中で多く(5つ)の抽象型を取るようなクラスを実装したい
時のことです。
// Foo~Quuxは、各々Traitとして定義されているものとする
trait Hoge[Foo, Bar, Baz, Qux, Quux] {
val foo: Foo = ???
val bar: Bar = ???
def baz: Baz = ???
def qux: Qux
def quux: Quux
}
2. 素朴な実装
素朴に書くと、上記trait Hoge
を継承する、 Hoge1
, Hoge2
, … クラスは
以下のような書き方になるかと思います。
// 型引数をいっぱい取るととても長い、イケてない実装に(右にスクロールして下さい…)
class Hoge1[Foo1 <: Foo, Bar1 <: Bar, Baz1 <: Baz, Qux1 <: Qux, Quux1 <: Quux]() extends Hoge[Foo1, Bar1, Baz1, Qux1, Quux1] {
override val baz = ???
// 以下はoverride修飾子を付けなくてもオーバーライドされる
override val qux = ???
override val quux = ???
}
case class Foo1() extends Foo
case class Bar1() extends Bar
case class Baz1() extends Baz
case class Qux1() extends Qux
case class Quux1() extends Quux
ただ上記の書き方だと、1行目のclass宣言が長くなりとても読みやすいコードとは言えません。
加えて、classの宣言が可読性が低い
+複雑に入り組んだ実装である
という背景から、以下のようなバグが混入することも起こり得ると思います。
// Bar1と間違えて、Bar2を抽象クラスの型引数に渡してしまっている
// この単純な実装例では、コンパイルが通ってしまう
// 3つ目(Baz)以降は簡略化のため???としています(本当は動きませんが、分かりやすさ重視で)
class Hoge1[Foo1 <: Foo, Bar2 <: Bar, ???]() extends Hoge[Foo1, Bar2, ???] {
override val baz = ???
override val qux = ???
override val quux = ???
}
また、型引数を扱うクラスが増えてくると、クラスHoge1
では正しくBar1
を型引数として渡したが、
別クラスFuga1
では誤ってBar2
を渡してしまった、といったややこしいことも起こり得ます。
3. 紹介したい実装
以下のような実装が良いと考えています。
step1.
まず、抽象クラスFoo
に対してその他の型をtype alias
として定義し、
具象クラスFoo1
で具体的なtype alias
の値を代入します。
// Fooの中で、Bar~Quuxをtypeとして定義する
trait Foo {
// TはTypeの略(抽象クラスと名前が被らないように)
type TBar <: Bar
type TBaz <: Baz
type TQux <: Qux
type TQuux <: Quux
}
// Foo1に対応する、Bar1~Quux1をオーバーライドする
case class Foo1() extends Foo {
type TBar = Bar1
type TBaz = Baz1
type TQux = Qux1
type TQuux = Quux1
}
step2.
抽象クラスHoge
で、trait Foo
から、各々のtype alias
へアクセスします。
※ Scalaでは、具象クラスのtype alias
へのアクセスは.
を使い、抽象クラスのtype alias
へのアクセスは#
を用います。
trait Hoge[Foo] {
val foo: Foo = ???
// 抽象クラスFooから、typeにアクセス
val bar: Foo#TBar = ???
def baz: Foo#TBaz = ???
def qux: Foo#TQux
def quux: Foo#TQuux
}
step3.
具象クラスHoge1
を定義します。必要な型引数はFoo
だけになっていることが分かります。
class Hoge1[Foo1 <: Foo]() extends Hoge[Foo1] {
override val baz = ???
override val qux = ???
override val quux = ???
}
4. まとめ
メリットとして、以下の2点が挙げられます。
利点1. 型引数が減ったことで読みやすい実装になった
2. 素朴な実装
で示した、SimpleManyTypeArgumentCoding.scala
と比較して、
3. 紹介したい実装
では、Hoge1.scala
の型引数が減ったことで読みやすい実装になりました。
利点2. 誤った型引数を渡してしまうリスクが軽減された
3. 紹介したい実装
のClass Hoge1
では、case class Foo1
のtype alias
に紐づいた型が使われ
2. 素朴な実装
で示したWrongTypeArgumentCoding.scala
のような
誤った型引数を渡してしまう
リスクが低くなります。
また、別クラスFuga1
でも、同様にcase class Foo1
のtype alias
に紐づいた型が使われていて
堅牢な型の縛りが効いていると言えます。
本編は以上です。
ご指摘ご意見等あればコメント宜しくお願いします。
5. 補足
読み飛ばしてしまって問題ないです。
Scalaの抽象クラス(今回の例でいえばtrait Hoge
)の実装を
具象クラス(今回の例でいえばclass Hoge1
)の実装でオーバーライドする場合には、
val
ではなく、def
を使うことが推奨されます。
理由はシンプルで、抽象クラス自体が初期化時されたタイミングで、val
が初期化されてしまうためです。
具象クラスがオーバーライドせずに、null
として初期化されることが起こり得るので推奨されません。
def
を用いて、具象クラスでオーバーライドするメソッドから呼ばれたタイミングで
初期化されるようにした方が無難でしょう。
また、上記の理由から、
抽象クラスでも、具象クラスの実装がオーバーライドされない場合はval
を用いても良いでしょう。
具象クラスで、抽象クラスの実装をオーバーライドする場合にはval
を用いた方が良いでしょう。
終わり