これは 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
と似ているように思えます。
何が嬉しいのか
値クラスのパフォーマンス問題の解決
単純な Id
や Password
といった情報は混同を防ぐためにラッパー型を定義することがあると思います。
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 のリリースが予定されているのは神様のイタズラでしょうか。とても楽しみですね。
今後の動向にも目を光らせていきたいと思います。