【Scala】Scala ~ 基本編 / ジェネリック <T> ~

■ はじめに

https://dk521123.hatenablog.com/entry/2023/03/16/012034

で YAMLファイルを扱った。ここで作成した
「例2:独自クラスにマッピングする」「例3:別YAML&リファクタリング」
を、どのクラスでも使えるようにしたい。

 そこで、その第一歩として、
今回は、Scala の Generics(ジェネリック/総称型)について学び
最後に曲がりなりにも上記の実装を実現する。

目次

【1】ジェネリック
【2】構文
 1)関数
 2)クラス
【3】ジェネリックの制限方法
 1)境界
 2)変位アノテーション
【4】サンプル
 例1:ジェネリッククラス
 例2:YAMLファイルを汎用的に独自クラスをマッピングする

【1】ジェネリック

* ジェネリック(Generic) のことを、Scala では
 型パラメータ(Type Parameter or Parameterized types)ともいう

【2】構文

1)関数

def 関数名[型パラメータ, ...](引数):戻り値 = {
  関数本体
}

2)クラス

class クラス名[A](クラス引数) {
 (フィールド定義|メソッド定義)*
}

【3】ジェネリックの制限方法

1)境界

境界種類 記法 指定可能なクラス
境界 [T] T そのものしか指定できない
上限境界 [T<: クラス名] Tには指定されたクラス又はそのサブクラスを指定可能
下限境界 [T>: クラス名] Tには指定されたクラス又はそのスーパークラスを指定可能
可視境界 [T<% クラス名] Tには指定されたクラス又はTに暗黙の型変換が可能なクラスを指定可能

2)変位アノテーション

class Foo[+A] // 共変クラス (covariance)
class Bar[-A] // 反変クラス(contravariance)
class Baz[A]  // 非変クラス (nonvariant)

// この「+/-」を「変位アノテーション (Variance Annotations)」という
変位種類 記法 指定可能なクラス 引数使用可否 戻り値使用可否
非変 [T] T に限られる YES YES
共変 [+T] T とそのサブクラスに限られる NO YES
反変 [-T] T とそのスーパークラスに限られる YES NO
* Java の配列は、共変
* Scala の配列は、非変

【3】サンプル

例1:ジェネリッククラス

https://docs.scala-lang.org/ja/tour/generic-classes.html

class Stack[T] {
  private var elements: List[T] = Nil

  def push(value: T): Unit =
    this.elements = value :: this.elements

  def peek: T = this.elements.head

  def pop(): T = {
    val currentTop = peek
    this.elements = this.elements.tail
    currentTop
  }
}

例2:YAMLファイルを汎用的に独自クラスをマッピングする

https://dk521123.hatenablog.com/entry/2023/03/16/012034

の「例3:別YAML&リファクタリング」をジェネリックで実装。

結構、苦労した、、、
なお、FileのCloseにUsingを使っているが、
AWS Glue の Scalaバージョンが 2.12なので、
代わりにTry-finallyで実装。

Main.scala

import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.constructor.Constructor
import scala.beans.BeanProperty
import java.io.FileInputStream
import scala.reflect._

object Main extends App {
  val inputPath = raw"C:\xxxx\process_info.yaml"
  val processInfo = readYaml[ProcessInfo](inputPath)
  println(processInfo)

  def readYaml[T: ClassTag](inputPath: String): T = {
    val reader = new FileInputStream(inputPath)
    try {
      // e.g. "class ProcessInfo"
      val targetClass = classTag[T].runtimeClass.toString()
      // e.g. "class ProcessInfo" -> "ProcessInfo"
      val className = targetClass.replaceFirst("class ", "")

      val inputYaml = new Yaml(new Constructor(className))
      return inputYaml.load(reader).asInstanceOf[T]
    } finally {
      reader.close()
    }
  }
}

class ProcessInfo {
  @BeanProperty var processName = ""
  @BeanProperty var schedule = ""
  @BeanProperty var emailListToNotify =
    new java.util.ArrayList[String]()
  @BeanProperty var subProcesses =
    new java.util.HashMap[String, java.util.HashMap[String, Any]]()
  override def toString: String =
      s"processName : $processName , schedule: $schedule, emailListToNotify : $emailListToNotify , subProcesses : $subProcesses "
}

process_info.yaml

processName: processA
schedule: 10:00-12:00
emailListToNotify:
  - sample@gmail
  - sample@yahoo.com
subProcesses:
  sub-processA1:
    inputPath: s3://your-bucket/sub-processA1/input/
    outputPath: s3://your-bucket/sub-processA1/output/
  sub-processA2:
    inputPath: s3://your-bucket/sub-processA2/input/
    outputPath: s3://your-bucket/sub-processA2/output/
  sub-processA3:
    inputPath: s3://your-bucket/sub-processA3/input/
    outputPath: s3://your-bucket/sub-processA3/output/

参考文献

https://atmarkit.itmedia.co.jp/ait/articles/1208/09/news128.html

関連記事

Scala ~ 環境構築編 ~
https://dk521123.hatenablog.com/entry/2023/03/10/193805
Scala ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2023/03/12/184331
ScalaYAML
https://dk521123.hatenablog.com/entry/2023/03/16/012034
Scala ~ ファイルハンドリング ~
https://dk521123.hatenablog.com/entry/2023/01/03/000000