18
6

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.

Dotty で入る Opaque Type Aliases について見ていく

Last updated at Posted at 2019-12-04

これは Scala Advent Calendar 5日目の記事です。

Dotty で入る新機能 opaque(オペーク by Google翻訳)について見ていきます。
https://dotty.epfl.ch/docs/reference/other-new-features/opaques.html

はじめに

Haskell の newtype について同僚と話す機会があり Dotty にも似たような機能が実装されるということを耳にしたので見てみることにしました。

事実、SIP にも Haskell の newtype に類似機能であるという言及がありました。
https://docs.scala-lang.org/sips/opaque-types.html

Haskell の newtype

Haskell の newtype を簡潔に説明すると 既存の型から別の型を定義する ではないでしょうか。

例えば以下のように Integer からオリジナルの MyInteger を生成します。

newtype MyInteger = MyInteger { getInteger :: Integer } deriving (Eq, Show)

新たに定義した型は、もとの型とは異なるので同値判定しようとしても当然エラーになります。

ghci>a = MyInteger 1
ghci>b = 1
ghci>a == a
True
ghci>b == b
True
ghci>a == b

<interactive>:11:6: error:
     No instance for (Num MyInteger) arising from a use of b
     In the second argument of (==), namely b
      In the expression: a == b
      In an equation for it: it = a == b

このままでは何もできませんが。型クラスのインスタンスを実装してやることで機能拡張が可能です。

-- Num を継承した型クラスのインスタンスを実装する
instance Num MyInteger where
  fromInteger = MyInteger x
  x + y       = MyInteger $ getInteger x + getInteger y
  x - y       = MyInteger $ getInteger x - getInteger y
  x * y       = MyInteger $ getInteger x * getInteger y
  abs x       = if getInteger x < 0 then x * (-x) else x
  signum x    = if getInteger x < 0 then -1 else 1

ghci> MyInteger 1 + MyInteger 2
-- MyInteger {getInteger = 3}

Scala の opaque

Scala の opaque も触ってみましたが確かに似ているなという印象で、個人的にはさらに柔軟なように感じました。

下記では、Int からオリジナルの MyInt を新たに定義しています。

object MyObject {

  opaque type MyInt = Int

  object MyInt {
    def apply(i: Int): MyInt = i
  }

  given MyIntOps: {
    def (x: MyInt) getInt: Int = x.toInt
    def (x: MyInt) + (y: MyInt): MyInt = MyInt(x + y)
    def (x: MyInt) * (y: MyInt): MyInt = MyInt(x *y)
  }
}

オブジェクトの実装ではファクトリーメソッドを定義し Given インスタンスからはレシーバーを提供します。

The notion of a companion object for opaque type aliases has been dropped.

ここでの object MyInt はコンパニオンオブジェクトという概念とは違うものらしい。なんて呼べば良いかわからないのでこの記事ではタイプオブジェクトと呼ぶことにします。

scala> import MyObject._

scala> MyInt(1) + MyInt(1)
// MyObject.MyInt = 2

scala> MyInt(1) * MyInt(1)
// MyObject.MyInt = 1

scala> MyInt(10).getInt
// Int = 10

当然ながら、新しく定義した型は、もとの型とは異なるため同じ型として扱うことはできません。

scala> val a = MyInt(1)
val a: MyObject.MyInt = 1

scala> val b = 1
val b: Int = 1

scala> a == b
1 |a == b
  |^^^^^^
  |Values of types MyObject.MyInt and Int cannot be compared with == or !=

この挙動は Haskell の newtype と似ているように思えます。

何が嬉しいのか

値クラスのパフォーマンス問題の解決

単純な IdPassword といった情報は混同を防ぐためにラッパー型を定義することがあると思います。

case class Id(v: String) extends AnyVal
case class Password(v: String) extends AnyVal

AnyVal を継承した定義は 値クラス と呼ばれ、実行時にインライン化してくれるのでボクシングのオーバーヘッドが発生しないという利点があります。

しかしながら、厳密にはメモリ割当てが必要なユースケースが存在するのも既知の事実でした。
https://docs.scala-lang.org/overviews/core/value-classes.html#when-allocation-is-necessary

代表的な例としてはタプルやコレクションに値クラスを入力した場合などです。

case class Label(v: String) extends AnyVal
val first = Label("first")
val second Label("second")

// メモリ食う
List(first, second)
(first, second)

値クラスのパフォーマンスに問題に対する言及についてはこちらの記事も面白かったです。
https://failex.blogspot.com/2017/04/the-high-cost-of-anyval-subclasses.html

これに対し、同様のことを opaque やろうとしたときにボクシングのオーバーヘッドが一切発生せず、メモリの消費についても改善されるようです。

型エイリアスとして機能しながらも拡張可能である

すでにコードでも紹介したとおり、型エイリアスの宣言に加えてタイプオブジェクトの実装や暗黙的な宣言を通じて拡張メソッドを定義できるというのは利点のように思えます。

これに関しては値クラスの機能と同等価値を提供できているといって良いでしょう。

情報隠蔽のしやすさ

値クラスないし、case class のデフォルトコンストラクタの隠蔽についてしばしば議論されることがありました。
https://qiita.com/petitviolet/items/b6af2877f64ebe8fe312

opaque はタイプオブジェクトにファクトリーメソッドを実装しない限りはデフォルトで生成する手段を提供しないので情報隠蔽のしやすさという点では適しているように思えます。

scala> object A {
     |   opaque type MyA = Int
     | }
// defined object A

scala> A.MyA
1 |A.MyA
  |^^^^^
  |value MyA is not a member of object A

以下のように case class の提供に制約を設けられるのはかなり嬉しいポイントだと思います。

scala> object MyContext {
     |   private case class Inside(v: Int)
     |   opaque type Outside = Inside
     |   object Outside {
     |     def apply(v: Int): Outside= Inside(v)
     |   }
     | }
// defined object MyContext

scala> MyContext.Outside(1)
val res0: MyContext.Outside = Inside(1)

scala> MyContext.Inside(1)
1 |MyContext.Inside(1)
  |^^^^^^^^^^^^^^^^
  |object Inside in object MyContext cannot be accessed as a member of MyContext.type from module class rs$line$4$.

アーティファクトの軽量化

値クラスと比較したときにアーティファクトのサイズが小さくなるということでした。
値クラスの場合はラッパークラスへのボクシングとそれの解除がコストになりますが、それとは対象的に opaque には固有のフットプリントがないのでアーティファクトが軽量化されるということでした。

まとめ

  • opaque は Haskell の newtype の類似機能。
  • opaque は値クラスの抱えていた課題を改善してくれるすごいやつ。
  • 情報隠蔽しやすい(個人的なお気に入り

最後に

東京オリンピックイヤーでもある翌年2020年に Dotty のリリースが予定されているのは神様のイタズラでしょうか。とても楽しみですね。

今後の動向にも目を光らせていきたいと思います。

おわり

18
6
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
18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?