New TypeとRefinedを使用したStronly Type Functionの作成


概要


スカラに重要な要素を選択させると、それはタイプ(Type Signature)です.タイプが間違っている場合は、コンパイルエラーが発生します.しかし、スカラーでもタイプを誤って使用し、弱いタイプの関数を生成して誤動作を引き起こす可能性があります.
ここでは、スカラーで記述された弱いタイプの関数を見て、コンパイル時に強いタイプの関数を作成して、さらにタイプを強制する方法を見てみましょう.

弱型関数


パラメータを使用してユーザー名とEメールユーザーを検索する関数を作成します.

通常、関数の初期値は上記のように考慮されます.ユーザー名はStringタイプ、emailはStringタイプのパラメータを設定します.ただし、タイプをStringとしても、正しいString値が得られる保証はありません.

ユーザー名とEメールはStringタイプでのみ強制でき、内容は強制できません.したがってlookup関数は弱いタイプ関数と呼ぶこともできるし、意図しないStringタイプの値と呼ぶこともできる.
では、同じタイプのパラメータを持つときに誤ったフォーマットの値を入力しないようにするにはどうすればいいのでしょうか.コンパイル時にStringタイプと無効なString値を入力できませんか?

Value Classを使用してより強力なタイプのチェックを行う関数を作成します。


まず、コンパイル時に値を強制することを後で考慮し、スカラー値クラス(scala.AnyVal)という抽象クラスを使用して実行時に有効な値のみをチェックすることをお勧めします.すなわち,ユーザ名はUsernameという値クラスタイプ,emailはEmailという値クラスタイプである.

しかし、エラー値が発生する可能性のある余地は依然として存在する.最終的には、値クラスも強いタイプの関数を作成できません.

インテリジェントコンストラクタを作成して、値クラスを作成するときに無効な値が含まれているかどうかを確認します.どうなりますか?したがって、Username、Emailはインテリジェントビルダーでしか生成できません.


しかし、ここにも問題があります.インテリジェントビルダーは、エラーの値をフィルタできますが、成果物はcaseクラスであるため、copyメソッドはフィールドをエラー値に汚染する可能性があります.

またcaseクラスが実現する値クラスにはホットスポットがある.メモリ割り当て(Memory Allocation)の問題です.スカラの公式報告書(https://docs.scala-lang.org/overviews/core/value-classes.html)には、Value Classに対してこのような説がある.
Because the JVM does not support value classes, Scala sometimes needs to actually instantiate a value class. Full details may be found in  SIP-15 .
Allocation Summary
A value class is actually instantiated when:
1. a value class is treated as another type.
2. a value class is assigned to an array.
3. doing runtime type tests, such as pattern matching.
JVMでは値クラスはサポートされていませんが、スカラーで値クラスを使用してオブジェクトを作成する必要がある場合があります.詳細については、SIP-15を参照してください.
メモリ割り当ての概要
値クラスは、実際には次の場合にオブジェクト作成ロジックを開始します.
1.異なるタイプの値クラスを使用する場合
2.値クラスを配列に割り当てる場合
3.パターンマッチングのように、実行時にタイプをチェックする場合
これはどういう意味ですか.1日から1つずつ見ていきましょう

1.Value Classが別のタイプで処理されたときにインスタンス化されます。


ここには特質を体現したバリュークラスがあります.

そしてMeterの作り方を作ります.
addという名称の方法はMeterを除き、多形性を形成するためにaddの方法のパラメータはDistanceである.でもここにはホットスポットがありますaddメソッドをバイトコードにコンパイルする.
// 바이트코드로 컴파일한 결과
[[syntax trees at end of                   cleanup]] // CaseClassExercise.scala
package io.icednut.strongtyped.step1 {
  object CaseClassExercise extends Object {
    def add(a: io.icednut.strongtyped.step1.Distance, b: io.icednut.strongtyped.step1.Distance): io.icednut.strongtyped.step1.Distance = {
      case <synthetic> val x1: io.icednut.strongtyped.step1.Distance = a;
        case5(){
          if (x1.$isInstanceOf[io.icednut.strongtyped.step1.Meter]())
            {
              <synthetic> val x2: Double = (x1.$asInstanceOf[io.icednut.strongtyped.step1.Meter]().value(): Double);
              {
                val value: Double = x2;
                matchEnd4(value)
              }
            }
          else
            case6()
        };
        case6(){
          matchEnd4(0.0)
        };
        matchEnd4(x: Double){
          x
        }
      };
      val bValue: Double = {
        case <synthetic> val x1: io.icednut.strongtyped.step1.Distance = b;
        case5(){
          if (x1.$isInstanceOf[io.icednut.strongtyped.step1.Meter]())
            {
              <synthetic> val x2: Double = (x1.$asInstanceOf[io.icednut.strongtyped.step1.Meter]().value(): Double);
              {
                val value: Double = x2;
                matchEnd4(value)
              }
            }
          else
            case6()
        };
        case6(){
          matchEnd4(0.0)
        };
        matchEnd4(x: Double){
          x
        }
      };
      new io.icednut.strongtyped.step1.Meter(aValue.+(bValue))
    };
    def execute(): Unit = {
      CaseClassExercise.this.add(new io.icednut.strongtyped.step1.Meter(1.0), new io.icednut.strongtyped.step1.Meter(2.0));
      ()
    };
    def <init>(): io.icednut.strongtyped.step1.CaseClassExercise.type = {
      CaseClassExercise.super.<init>();
      ()
    }
  };
  abstract trait Distance extends Object;
  final case class Meter extends Object with io.icednut.strongtyped.step1.Distance with Product with Serializable {
    ...
  };
  <synthetic> object Meter extends scala.runtime.AbstractFunction1 with Serializable {
    ...
  }
}
バイトコードに変換するaddメソッドのパラメータから見ると、Distanceタイプである.問題があると思うかもしれませんが、addメソッドを次のように変更し、バイトコードにコンパイルすると状況が異なります.
[[syntax trees at end of                   cleanup]] // CaseClassExercise.scala
package io.icednut.strongtyped.step1 {
  object CaseClassExercise extends Object {
    def add(a: Double, b: Double): Double = {
      val aValue: Double = a;
      val bValue: Double = b;
      aValue.+(bValue)
    };
    def execute(): Unit = {
      val m: Double = 5.0;
      val array: Array[io.icednut.algorithm_exercise.leetcode.strongtyped.step1.Meter] = scala.Array.apply(scala.Predef.genericWrapArray(Array[io.icednut.algorithm_exercise.leetcode.strongtyped.step1.Meter]{new io.icednut.algorithm_exercise.leetcode.strongtyped.step1.Meter(m)}), (ClassTag.apply(classOf[io.icednut.algorithm_exercise.leetcode.strongtyped.step1.Meter]): scala.reflect.ClassTag)).$asInstanceOf[Array[io.icednut.algorithm_exercise.leetcode.strongtyped.step1.Meter]]();
      ()
    };
    def <init>(): io.icednut.algorithm_exercise.leetcode.strongtyped.step1.CaseClassExercise.type = {
      CaseClassExercise.super.<init>();
      ()
    }
  };
  abstract trait Distance extends Object;
  final case class Meter extends Object with io.icednut.strongtyped.step1.Distance with Product with Serializable {
    ...
  };
  <synthetic> object Meter extends scala.runtime.AbstractFunction1 with Serializable {
    ...
  }
}
パラメータタイプをサブタイプとするaddメソッドのコンパイル結果を表示すると、Double値に変換されることがわかります.すなわち、AnyVal case classはvalueフィールドに直接変換される.多形性のためにtraitを使用すると、かえってメモリのオーバーヘッドが発生します.

2.値クラスを配列に割り当てるときにインスタンス化します。


上記では、Meter値クラスを使用してアレイを宣言します.

これらのスカラーコードをコンパイルしてバイトコードに変換すると、別の問題が発生します.
[[syntax trees at end of                   cleanup]] // CaseClassExercise.scala
package io.icednut.strongtyped.step1 {
  object CaseClassExercise extends Object {
    def execute(): Unit = {
      val m: Double = 5.0;
      val array: Array[io.icednut.strongtyped.step1.Meter] = scala.Array.apply(scala.Predef.genericWrapArray(Array[io.icednut.strongtyped.step1.Meter]{new io.icednut.strongtyped.step1.Meter(m)}), (ClassTag.apply(classOf[io.icednut.strongtyped.step1.Meter]): scala.reflect.ClassTag)).$asInstanceOf[Array[io.icednut.strongtyped.step1.Meter]]();
      ()
    };
    def <init>(): io.icednut.strongtyped.step1.CaseClassExercise.type = {
      CaseClassExercise.super.<init>();
      ()
    }
  };
  abstract trait Distance extends Object;
  final case class Meter extends Object with io.icednut.strongtyped.step1.Distance with Product with Serializable {
    ...
  };
  <synthetic> object Meter extends scala.runtime.AbstractFunction1 with Serializable {
    ...
  }
}
mというローカル変数はDoubleに変換されたが、Array[Meter]という配列はMeterに変換されず、Doubleクラスに実例化されたコードが生成された.メモリのオーバーヘッドともいえる.

3.パターンマッチングチェックタイプを使用して値クラスをインスタンス化します。

Meterメソッドにモードマッチングコードを追加し、検査値の論理を補完する.

このモードマッチングコードをコンパイルすると、addで作成され、チェックされたコードが表示されます.
[[syntax trees at end of                   cleanup]] // CaseClassExercise.scala
package io.icednut.strongtyped.step1 {
  object CaseClassExercise extends Object {
    def add(a: Double, b: Double): Double = {
      val aValue: Double = {
        case <synthetic> val x1: Double = a;
        case4(){
          if (new io.icednut.algorithm_exercise.leetcode.strongtyped.step1.Meter(x1).ne(null))
            {
              val value: Double = x1;
              if (scala.Double.box(value).!=(null).&&(value.>(0.0)))
                matchEnd3(value)
              else
                case5()
            }
          else
            case5()
        };
        case5(){
          matchEnd3(0.0)
        };
        matchEnd3(x: Double){
          x
        }
      };
      val bValue: Double = {
        case <synthetic> val x1: Double = b;
        case4(){
          if (new io.icednut.algorithm_exercise.leetcode.strongtyped.step1.Meter(x1).ne(null))
            {
              val value: Double = x1;
              if (scala.Double.box(value).!=(null).&&(value.>(0.0)))
                matchEnd3(value)
              else
                case5()
            }
          else
            case5()
        };
        case5(){
          matchEnd3(0.0)
        };
        matchEnd3(x: Double){
          x
        }
      };
      aValue.+(bValue)
    };
	};
  abstract trait Distance extends Object;
  final case class Meter extends Object with io.icednut.strongtyped.step1.Distance with Product with Serializable {
    ...
  };
  <synthetic> object Meter extends scala.runtime.AbstractFunction1 with Serializable {
    ...
  }
}
バイトコードのMeterメソッドでは、タイプを確認するために、Meterで一度インスタンス化し、正常に動作しているかどうかを確認します.addMeterに置き換えられたので、パターンマッチングコードもDoubleに置き換えられ、値クラスのみを抽出するコードにコンパイルされる見込みで、現在Double例が生成されている.メモリのオーバーヘッドともいえる.
このようにMeterおよびcase classで実装される値クラスは、何気なく使用される多形性および配列、モードマッチングコードにメモリオーバーヘッドをロードする可能性がある.メモリの使用量が低いことに問題があると思われるかもしれませんが、実際の高容量サービスでは、このようなメモリの浪費が反感を買う可能性があります.

タイプを、通常のクラスを使用して実装される値クラスに強制します。


では、値クラスをケースクラスではなく普通のクラスにすればいいのではないでしょうか.一般クラスおよびインテリジェントビルダーを使用した最終結果は、次のとおりです.


しかし、ここにもホットスポットがあります.通常のclassではモードマッチングができません.

もちろん、以下のように譲歩モードマッチングコードを譲歩することもできます.

ただし、case classのように強力なモードマッチングコードを使用してフィールドにアクセスすることはできません.一般的なパターンマッチングコードが冗長になりました.
通常のクラスのようにメモリのオーバーヘッドがなく、キャビネットクラスのように、モードマッチングの面で、コード実装もきれいな値クラスを作成することができますか?この問題の答えがあってよかった.Newtypeです.

New Typeで値クラスを実装&コンパイル時に値コンテンツを強制実行


NewTypeを使用して実装される値クラスは次のとおりです.
libraryDependencies += "io.estatico" %% "newtype" % "0.4.4"

コードでは、caseクラスにのみnewtypeという構文が追加され、マクロのため、構文はコンパイル時に次のように変換されます.

Scaraのcaseクラスとは異なり、New Typeで実装されたcaseクラスにはコピー方法や追加方法がないため、コンパイル後の結果ファイルサイズも減少し、値クラスのように使用することができる.
さらに、Refinedというマクロ・ライブラリを使用すると、コンパイル時にタイプだけでなくコンテンツもチェックされ、コンパイルエラーが発生します.そうです.
libraryDependencies += "eu.timepit" %% "refined" % "0.9.27"

したがって、New TypeとRefinedを使用してより軽い値クラスを作成し、コンパイル時に値の内容をチェックすることができます.したがって、最初に表示したUsernameとEmailの例を作成できます.



サスペンス

  • New TypeとRefinedの動作原理は何ですか?コードはいついつそのように変換されますか?
  • RuntimeでRefinedで値チェックをする場合はどうすればいいですか?
  • スマートフォンでは、String Refined Contentsのタイプ認識があまりよくありません.知識人の話題のようですが、解決策はありますか?
  • 参考資料

  • https://github.com/estatico/scala-newtype
  • https://github.com/fthomas/refined
  • https://blog.rockthejvm.com/refined-types/
  • http://fthomas.github.io/talks/2016-05-04-refined/#1
  • https://blog.softwaremill.com/a-simple-trick-to-improve-type-safety-of-your-scala-code-ba80559ca092