1. 概要

ドメイン固有言語(DSL)は、特定の有限ドメインの問題を解決することに特化したプログラミング言語です。

内部DSLがホストされているため、言語を解釈するためのツールを作成する必要がありません。 しかし、ホスト言語は私たちのDSLの限界を定義します。 JavaやC#のような厳格な言語では、私たちができる最善のことは、流暢なインターフェースを備えたライブラリーです。

Scalaの柔軟な構文により、DSLの理想的なホストになります。このチュートリアルでは、次の代わりに20秒を記述できるDSLを記述します。

new FiniteDuration(20, SECONDS)

2. 注意の言葉

scala.concurrent.duration の実装は、Scalaの2.10より前のバージョンと互換性があり、暗黙の変換を使用します。 代わりに暗黙のクラスを使用します。

どちらのアプローチも同等ですが、暗黙のクラスは面倒で安全ではありません。

3. しかし、それはどのように機能しますか?

シンタックスシュガーを作成するには、少なくとも2つのことを行う必要があります。ラッパークラスを作成し、それを暗黙的にすることです。 その後、言語自体が私たちにたくさんの砂糖を与えてくれます。

3.1. ラッパークラス

さあ、スプーン一杯の砂糖、ラッパークラスを書いてみましょう。

package com.baeldung.scala

import scala.concurrent.duration.{FiniteDuration, MILLISECONDS, MINUTES, SECONDS}

class DurationSugar(time: Long) {
  def milliseconds: FiniteDuration = new FiniteDuration(time, MILLISECONDS)

  def seconds: FiniteDuration = new FiniteDuration(time, SECONDS)

  def minutes: FiniteDuration = new FiniteDuration(time, MINUTES)
}

今、私たちは書くことができます:

(new DurationSugar(20)).seconds

これは改善のようには見えません。 しかし、時間などのより多くの変換のためのメソッドをホストするモジュールができました。

ちなみに、すべてのメンバーパッケージではなく、scala.concurrent.durationのメンバーを明示的にインポートする必要があることに注意してください。

3.2. 暗黙のクラスの使用

DurationSugar は引数を1つだけ取り、暗黙としてマークすることができます。 残念ながら、コンパイラは、暗黙のクラスをトップレベルの定義にすることはできないと文句を言います。 また、自分の言語をどのように使用したいかについても考える必要があります。

理想的には、1回のインポートで、必要なものすべてをスコープに含める必要があります。 私たちの目標は、次のように記述できるようにすることです。

import com.baeldung.scala.durationsugar._

object Main {
  println(20 seconds)
}

これは、クラスをパッケージオブジェクトに移動し、暗黙的にすることで実現できます。 これにより、 20.seconds()と書くことができます。

package object durationsugar {

  implicit class DurationSugar(time: Long) {
    def milliseconds: FiniteDuration = new FiniteDuration(time, MILLISECONDS)

    def seconds: FiniteDuration = new FiniteDuration(time, SECONDS)

    def minutes: FiniteDuration = new FiniteDuration(time, MINUTES)
  }
}

暗黙的なクラスの名前は、ユーザースコープ内のオブジェクト、メソッド、またはメンバーと衝突しないようにする必要があります。 このため、ケースクラスにすることはできませんが、明示的にインスタンス化することは決してないため、ユーザーのコードとの名前の衝突を避けるために、長い名前の側で誤りを犯す必要があります。

コンストラクターで複数の引数を持つ暗黙のクラスを宣言しないように注意する必要があります。 Scalaはそれを許可しますが、暗黙的なルックアップ中には使用しないため、標準クラスと同じになります。

3.3. 残りは無料です

Scalaは私たちから他に何も必要とせず、残りの砂糖は無料で入手できます。

パラメータなしのメソッドの呼び出しから括弧を省略して、次のように記述できます。

"20.seconds" should "equal the object created with the native scala sugar" in {
  20.seconds shouldBe new FiniteDuration(20, SECONDS)
}

およびセミコロンがないためにコンパイラが混乱し、 20秒と記述できる場合を除いて、ドットを省略できます。

ただし、通常、ドットのない構文は避ける必要があります。これは、特定の状況下で、メソッドの1つに続くコードが増えると、コンパイラが混乱する可能性があるためです。 また、コンパイラのエラーメッセージは混乱を招く可能性があります。

4. 甘くする

暗黙のクラスを使用して、凝ったコンストラクター以上のことを行うことができます。 それらを使用すると、新しいメソッドを使用して、制御外のクラスを拡張できます。

implicit class DurationOps(duration: FiniteDuration) {
  def ++(other: FiniteDuration): FiniteDuration =
    (duration.unit, other.unit) match {
      case (a, b) if a == b =>
        new FiniteDuration(duration.length + other.length, duration.unit)
      case (MILLISECONDS, _) =>
        new FiniteDuration(duration.length + other.toMillis, MILLISECONDS)
      case (_, MILLISECONDS) =>
        new FiniteDuration(duration.toMillis + other.length, MILLISECONDS)
      case (SECONDS, _) =>
        new FiniteDuration(duration.length + other.toSeconds, SECONDS)
      case (_, SECONDS) =>
        new FiniteDuration(duration.toSeconds + other.length, SECONDS)
    }
}

ラップしているクラスにはすでに+という名前のメソッドがあるため、++を使用する必要があります。

MILLISECONDSSECONDSに異なるインスタンスがないため。 パターンマッチングを使用してユニットを区別し、追加する前に小さい方のユニットに変換する必要があります。

そして、Scalaの柔軟な命名規則のおかげで、演算子のように見えるメソッドでクラスを拡張できます。

"20.seconds ++ 30.seconds" should "be equal to 50.seconds" in {
  20.seconds ++ 30.seconds shouldBe 50.seconds
}

"20.seconds ++ 1.minutes" should "be equal to 80.seconds" in {
  20.seconds ++ 1.minutes shouldBe 80.seconds
}

Scalaにはメソッドと演算子の区別がないことを決して忘れてはなりません。 メソッド名に豊富な文字セットを使用し、同じ柔軟な構文規則を使用して、型に対する特別な操作のように見えるコードを記述できるようにするだけです。

5. 結論

Scalaの暗黙のクラスは、既存の型に機能を追加するための効率的な方法です。 これらにより、Javaの流暢なインターフェース標準を超えるホストされたミニ言語を作成できます。

必要な機能は3つだけですが、ScalaのDSL機能はそれだけではありません。 DSLの作成に役立つその他の機能は、コンパニオンオブジェクト、applyメソッド、高階関数、ラムダ関数です。 括弧を中括弧に置き換えて、1つの引数のみを受け入れるメソッドを呼び出すオプションもあります。

いつものように、記事の完全なソースコードは、GitHubから入手できます。