7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ぷりぷりあぷりけーしょんずAdvent Calendar 2019

Day 5

【Scala】トレイト・型パラメータ周りで勘違いしていた言語仕様

Last updated at Posted at 2019-12-06

この記事はぷりぷりあぷりけーしょんず Advent Calendar 2019の5日目の記事です。

はじめに

遅刻しました。ごめんなさい。
書こうと思っていたネタの説明用に Scala のサンプルコードを書いていたら言語仕様を勘違いしていたため間に合いませんでした。
なので、急遽私が何を勘違いしていたかのネタで記事を書きます。

ちなみに普段の仕事は最近『データアーキテクト』と呼ばれているようなことをしています。
が、まだ浸透していない言葉なので『データエンジニア』を自称しています。

勘違い①『兄弟トレイトを複数ミックスした場合、明示的に指定した型の方のメソッド以外は見えなくなると思っていた』

サンプルコード見ないとわかりにくですね。

Main1.scala
package com.puripuri.example

trait ParentTrait {
  def hoge(): Unit
}

trait ChildTraitA extends ParentTrait {
  override def hoge(): Unit = {
    methodA()
  }

  def methodA(): Unit
}

trait ChildTraitB extends ParentTrait {
  override def hoge(): Unit = {
    methodB()
  }

  def methodB(): Unit
}

case class ImplA() extends ChildTraitB with ChildTraitA {
  def methodA(): Unit = {
    println("TraitA")
  }

  def methodB(): Unit = {
    println("TraitB")
  }
}

case class ImplB() extends ChildTraitA with ChildTraitB {
  def methodA(): Unit = {
    println("TraitA")
  }

  def methodB(): Unit = {
    println("TraitB")
  }
}

object Main extends App {
  ImplA().hoge() // TraitA
  ImplB().hoge() // TraitB

  val a: ChildTraitA = ImplA()
  val b: ChildTraitB = ImplA()

  a.hoge() // TraitA
  b.hoge() // TraitB になると思っていたら TraitA だった
}

サンプルコードの ChildTraitAChildTraitB は同じ ParentTrait を親に持ちます。
ChildTraitA と ChildTraitB の両方のトレイトをクラスにミックスインした場合、『線形化1』の仕組みにより後からミックスインしたトレイトのメソッドが有効になります。サンプルコードの場合の hoge() メソッドのことです。
というところまでは理解していました。

ただし、インスタンス化する際に型を明示的にした場合は明示的に指定した型が持つメソッドが有効になる。
と思っていたのですがこれは勘違いでした。

実際はインスタンス化の際に指定した型の種類を問わず線形化のルールが適用されます。
サンプルコードの実行結果は以下の通り。

実行結果.txt
TraitA
TraitB
TraitA
TraitA

一番下の実行結果は ChildTraitB 型でインスタンス化したので methodB() のみが見えるようになって結果は "TraitB" と表示されるものと思っていました。

勘違い②『型パラメータを持つトレイトも普通に複数ミックスインできると思っていた』

こちらもまずはサンプルコードをペタリ

Main2.scala
package com.puripuri.example

trait ParentTrait[A] {
  def hoge(): A
}

trait ChildTraitA extends ParentTrait[String] {
  override def hoge(): String = {
    methodA()
  }

  def methodA(): String
}

trait ChildTraitB extends ParentTrait[Int] {
  override def hoge(): Int = {
    methodB()
  }

  def methodB(): Int
}

case class ImplA() extends ChildTraitB with ChildTraitA {
  def methodA(): String = {
    "TraitA"
  }

  def methodB(): Int = {
    12345
  }
}

case class ImplB() extends ChildTraitA with ChildTraitB {
  def methodA(): String = {
    "TraitA"
  }

  def methodB(): Int = {
    12345
  }
}

object Main2 extends App {
  println(ImplA().hoge())
  println(ImplA().hoge().getClass) // String だと思ってた
  println(ImplB().hoge())
  println(ImplB().hoge().getClass) // Int だと思ってた

  val a: ChildTraitA = ImplA()
  val b: ChildTraitB = ImplA()

  println(a.hoge())
  println(a.hoge().getClass) // String だと思ってた
  println(b.hoge())
  println(b.hoge().getClass) // ①の勘違いをしていたので Int だと思ってた
}

こちらのコードは①の勘違いに付随する形で勘違いしていたものです。
このコードでは私は線形化の仕組みにより ImplA().hoge() は String を返し、ImplB().hoge() は Int を返すものと思っていました。
しかし、このコードは以下の 2 つのメッセージが出てコンパイルエラーとなります。

Error:(23, 12) illegal inheritance;
 class ImplA inherits different type instances of trait ParentTrait:
com.puripuri.example.ParentTrait[String] and com.puripuri.example.ParentTrait[Int]
case class ImplA() extends ChildTraitB with ChildTraitA {
Error:(33, 12) illegal inheritance;
 class ImplB inherits different type instances of trait ParentTrait:
com.puripuri.example.ParentTrait[Int] and com.puripuri.example.ParentTrait[String]
case class ImplB() extends ChildTraitA with ChildTraitB {

これは完全に勘違いしていました。
型パラメータの型が違う場合はそもそも同時にミックスインできない仕様のようです。
海外でも全く同じ落とし穴にハマっている方がいるようでした。(10 年前に……)
https://www.scala-lang.org/old/node/3423

型パラメータが同じならミックスイン可能

ただし、型パラメータで指定した型が全く同じであればミックスイン可能です。

Main3.scala
package com.puripuri.example

trait ParentTrait[A] {
  def hoge(): A
}

trait ChildTraitA extends ParentTrait[String] {
  override def hoge(): String = {
    methodA()
  }

  def methodA(): String
}

trait ChildTraitB extends ParentTrait[String] {
  override def hoge(): String = {
    methodB()
  }

  def methodB(): String
}

case class ImplA() extends ChildTraitB with ChildTraitA {
  def methodA(): String = {
    "TraitA"
  }

  def methodB(): String = {
    "TraitB"
  }
}

case class ImplB() extends ChildTraitA with ChildTraitB {
  def methodA(): String = {
    "TraitA"
  }

  def methodB(): String = {
    "TraitB"
  }
}

object Main3 extends App {
  println(ImplA().hoge())
  println(ImplA().hoge().getClass)
  println(ImplB().hoge())
  println(ImplB().hoge().getClass)

  val a: ChildTraitA = ImplA()
  val b: ChildTraitB = ImplA()

  println(a.hoge())
  println(a.hoge().getClass)
  println(b.hoge())
  println(b.hoge().getClass)
}

このコードの実行結果は以下の通りです。

TraitA
class java.lang.String
TraitB
class java.lang.String
TraitA
class java.lang.String
TraitA
class java.lang.String

①のときと同じように線形化されています。

まとめ

雰囲気エンジニアなので普段そこまで技術を突き詰めませんが、せっかくのアドベントカレンダーなので色々と検証コードを書いてみました。
Scala を 1 年以上使ってきて言語仕様を『完全に理解2』しているものの使っていて『何も分からない2』と日々思っています。
が、便利な機能がたくさんあり Java のライブラリなども基本的にそのまま使用可能なのでちゃんと使えるようになれば書いていて楽しい言語だと思います。
最近流行の関数型のパラダイムも含んだ言語ですし。

言語仕様の複雑さとマルチパラダイムという性質の関係でプログラミング初心者には全くオススメできない言語ですが第二言語としては良いんじゃないかなーと思います。

私が遅刻した関係で既に投稿されていますが 6 日目は @ya_mori さんの記事です。

  1. 【参考リンク】。線形化のためにはミックスインしたクラスのメソッドに override 修飾子が必須

  2. https://twitter.com/kaitendaentai/status/1052689241744896001 2

7
0
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
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?