0Pricing
Scala for Backend Engineering & Functional Programming · レッスン

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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. ADTによるモデル化
  2. Smart Constructors
  3. Newtype
  4. ドメインの合成
← Scala for Backend Engineering & Functional Programmingに戻る