Newtype
型安全なラッパー
「Newtype」はCoddyKit上の無料Scala for Backend Engineering & Functional Programmingレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはScala for Backend Engineering & Functional Programming学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Scala for Backend Engineering & Functional Programmingコースには全4レッスンが含まれています。
型安全なラッパー
newtypeは、単一の基底値をラップする独立した型です。UserIdとProductIdのように、どちらもIntで表現される値の取り違えを防ぎます。
case class UserId(value: Int)
case class ProductId(value: Int)
object Main:
def main(args: Array[String]): Unit =
val u = UserId(1)
val p = ProductId(1)
println(u.value == p.value) // values equal, types differプリミティブ型偏重の問題
プリミティブ型偏重とは、ドメイン上の概念にStringやIntを過度に使うことです。その結果、名前が必要な場所にメールアドレスを渡すといった不自然な操作が可能になります。newtypeは、それぞれの概念に固有の型を与えることで、この問題を解決します。
case class Email(value: String)
case class City(value: String)
object Main:
def greet(c: City): String = s"Welcome to ${c.value}"
def main(args: Array[String]): Unit =
println(greet(City("Berlin")))Case ClassによるNewtype
最も単純なnewtypeは、フィールドを1つだけ持つcase classです。等価性、読みやすいtoString、パターンマッチングを標準で利用できます。代わりに、値ごとに1つのオブジェクトが割り当てられます。
case class OrderId(value: String)
object Main:
def main(args: Array[String]): Unit =
val id = OrderId("ORD-42")
println(id)
println(id.value)Opaque TypeによるNewtype
割り当てをゼロにするには、opaque typeでnewtypeを実装します。実行時には基底型として振る舞い、コンパイル時には独立した型になります。
object Ids:
opaque type UserId = Int
def apply(i: Int): UserId = i
extension (u: UserId) def value: Int = u
object Main:
def main(args: Array[String]): Unit =
val u = Ids(7)
println(u.value)Extensionによる振る舞いの追加
extensionメソッドでnewtypeに操作を追加し、ドメイン上の概念に適した機能だけを公開します。
object Money:
opaque type Cents = Long
def apply(n: Long): Cents = n
extension (c: Cents)
def +(o: Cents): Cents = c + o
def toDollars: Double = c / 100.0
object Main:
def main(args: Array[String]): Unit =
val total = Money(250) + Money(750)
println(total.toDollars)関数シグネチャでのNewtype
newtypeを使うと、シグネチャが意図を自ら説明し、引数の順序に関するバグをコンパイル時に検出できます。異なるnewtypeのパラメーターを誤って入れ替えることはできません。
case class Width(value: Int)
case class Height(value: Int)
object Main:
def area(w: Width, h: Height): Int = w.value * h.value
def main(args: Array[String]): Unit =
println(area(Width(4), Height(5)))コレクションでのNewtype
newtypeをキーにしたMapは、Intをそのままキーにするより明確で安全です。型によって、誤った種類のキーを使うことが防止されます。
case class UserId(value: Int)
object Main:
def main(args: Array[String]): Unit =
val names = Map(UserId(1) -> "Ada", UserId(2) -> "Bob")
println(names(UserId(2)))検証済みのNewtype
newtypeとスマートコンストラクターを組み合わせると、ラッパーにも不変条件を適用できます。ここではNonEmptyStringが空になることはありません。
case class NonEmptyString private (value: String)
object NonEmptyString:
def of(s: String): Option[NonEmptyString] =
if s.nonEmpty then Some(NonEmptyString(s)) else None
object Main:
def main(args: Array[String]): Unit =
println(NonEmptyString.of("hi"))
println(NonEmptyString.of(""))表現方法の選択
必要に応じて選択します。
- case class:最も簡単で、パターンマッチングが可能ですが、オブジェクトを割り当てます。
- opaque type:コストがゼロでボックス化もなく、頻繁に実行される処理や大規模なコレクションに適しています。
object Temp:
opaque type Kelvin = Double
def apply(d: Double): Kelvin = d
extension (k: Kelvin) def value: Double = k
object Main:
def main(args: Array[String]): Unit =
val readings = List(Temp(300.0), Temp(310.5))
println(readings.map(_.value).sum)意図しない変換を防ぐ
同じ基底型を持つ2つのnewtypeは、暗黙には変換されません。両者の間を移動するには明示的な関数を記述するため、意図をコードに残せます。
case class Meters(value: Double)
case class Feet(value: Double)
object Main:
def toFeet(m: Meters): Feet = Feet(m.value * 3.281)
def main(args: Array[String]): Unit =
println(toFeet(Meters(2.0)))Newtypeを使う場面
プリミティブ型にドメイン上の意味がある場合は、newtypeを使いましょう。
- 識別子、単位、形式が決まった文字列。
- 簡潔さを優先するならcase class、パフォーマンスを優先するならopaque typeを使います。
- 不変条件がある場合はスマートコンストラクターを追加します。
object Domain:
opaque type Sku = String
def of(s: String): Option[Sku] =
if s.startsWith("SKU-") then Some(s) else None
extension (k: Sku) def raw: String = k
object Main:
def main(args: Array[String]): Unit =
println(Domain.of("SKU-9").map(_.raw))理解度チェック
newtypeについての理解度を確認しましょう。
まとめ
newtypeについて学びました。
- newtypeは、1つの基底値を独立した型でラップします。
- プリミティブ型偏重を解消し、引数の取り違えを防ぎます。
- 簡潔さには
case class、コストゼロにはopaque typeを使います。 - 検証済みのラッパーにはスマートコンストラクターを追加します。
- newtype間の変換は常に明示的に行います。
object Ids:
opaque type AccountId = Long
def apply(n: Long): AccountId = n
extension (a: AccountId) def value: Long = a
object Main:
def main(args: Array[String]): Unit =
val acc = Ids(1001L)
println(acc.value)よくある質問
「Newtype」レッスンは無料ですか?
はい。「Newtype」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Scala for Backend Engineering & Functional Programmingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Scala for Backend Engineering & Functional Programmingコースには全4レッスンが含まれています。
「Newtype」で何を学びますか?
型安全なラッパー ブラウザで直接実行するハンズオンコードでScala for Backend Engineering & Functional Programmingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Scala for Backend Engineering & Functional Programmingを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのScala for Backend Engineering & Functional Programmingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「Newtype」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このScala for Backend Engineering & Functional Programmingレッスンでコードを書いて実行できますか?
はい。すべてのScala for Backend Engineering & Functional Programmingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。