LoginSignup
3
2

More than 5 years have passed since last update.

Scalaで多くの型引数が登場する複雑なクラスを実装する

Last updated at Posted at 2019-04-06

1. はじめに

Scalaを用いたアプリケーションコードでは、型パラメータを用いたくなることがあるかと思いますが
その中でも特に、多く(例えば5つ)の型引数が登場するようなクラスを実装したい
というモチベーションが湧くことがあり、この記事ではその実装の工夫について紹介してみようと思います。

具体的なコードを書いて例を示すと、以下のような
抽象クラスの中で多く(5つ)の抽象型を取るようなクラスを実装したい時のことです。

ManyTypeArgumentCoding.scala
// 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, … クラスは
以下のような書き方になるかと思います。

SimpleManyTypeArgumentCoding.scala
// 型引数をいっぱい取るととても長い、イケてない実装に(右にスクロールして下さい…)
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の宣言が可読性が低い複雑に入り組んだ実装である
という背景から、以下のようなバグが混入することも起こり得ると思います。

WrongTypeArgumentCoding.scala
// 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.scala
// Fooの中で、Bar~Quuxをtypeとして定義する
trait Foo {
  // TはTypeの略(抽象クラスと名前が被らないように)
  type TBar <: Bar
  type TBaz <: Baz
  type TQux <: Qux
  type TQuux <: Quux
}
Foo1.scala
// 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へのアクセスは#を用います。

Hoge.scala
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だけになっていることが分かります。

Hoge1.scala
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 Foo1type aliasに紐づいた型が使われ
2. 素朴な実装で示したWrongTypeArgumentCoding.scalaのような
誤った型引数を渡してしまうリスクが低くなります。

また、別クラスFuga1でも、同様にcase class Foo1type aliasに紐づいた型が使われていて
堅牢な型の縛りが効いていると言えます。

本編は以上です。
ご指摘ご意見等あればコメント宜しくお願いします。

5. 補足

読み飛ばしてしまって問題ないです。

Scalaの抽象クラス(今回の例でいえばtrait Hoge)の実装を
具象クラス(今回の例でいえばclass Hoge1)の実装でオーバーライドする場合には、
valではなく、defを使うことが推奨されます。

理由はシンプルで、抽象クラス自体が初期化時されたタイミングで、valが初期化されてしまうためです。
具象クラスがオーバーライドせずに、nullとして初期化されることが起こり得るので推奨されません。

defを用いて、具象クラスでオーバーライドするメソッドから呼ばれたタイミングで
初期化されるようにした方が無難でしょう。

また、上記の理由から、
抽象クラスでも、具象クラスの実装がオーバーライドされない場合はvalを用いても良いでしょう。
具象クラスで、抽象クラスの実装をオーバーライドする場合にはvalを用いた方が良いでしょう。

終わり

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