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

ADTによるモデル化

不正な状態を表現不可能にします

「ADTによるモデル化」はCoddyKit上の無料Scala for Backend Engineering & Functional Programmingレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはScala for Backend Engineering & Functional Programming学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Scala for Backend Engineering & Functional Programmingコースには全4レッスンが含まれています。

代数的データ型

代数的データ型(ADT)は、関数型のドメインモデリングの基盤です。積型(AND)と和型(OR)を組み合わせて、データを正確に表現します。

  • 積型:複数のフィールドを持つレコード。
  • 和型:複数のバリアントからの選択。
case class Point(x: Int, y: Int) // product type

object Main:
  def main(args: Array[String]): Unit =
    println(Point(1, 2))

積型

積型は値をまとめたものです。Scalaではcase classが積型にあたり、インスタンスはすべてのフィールドを同時に保持します。

case class User(name: String, age: Int)

object Main:
  def main(args: Array[String]): Unit =
    val u = User("Ada", 36)
    println(u.name)
    println(u.age)

和型

和型は、複数のバリアントのうち正確に1つである値です。Scala 3のenumはこれを直接表現します。

enum PaymentMethod:
  case Cash
  case Card(number: String)
  case Crypto(wallet: String)

object Main:
  def main(args: Array[String]): Unit =
    val p: PaymentMethod = PaymentMethod.Card("1234")
    println(p)

不正な状態を表現不能にする

核心となる原則は、不正なデータをそもそも構築できないように型を設計することです。型システムが不正な状態を禁止すれば、その状態を実行時に検査する必要はありません。

enum Connection:
  case Disconnected
  case Connected(sessionId: String)

object Main:
  def main(args: Array[String]): Unit =
    // No way to have a sessionId while Disconnected
    val c: Connection = Connection.Connected("abc")
    println(c)

悪い設計と良い設計

nullableなフィールドを持つ平坦なレコードは、不正な状態を招きます。それぞれのケースをバリアントとしてモデル化すれば、不正な状態をなくせます。

  • 悪い例:case class Conn(connected: Boolean, sessionId: String)では、connected=falseなのにsessionIdが設定された状態を許してしまいます。
  • 良い例:和型によって、フィールドを正しい状態に結び付けます。
enum Door:
  case Open
  case Closed
  case Locked(key: String)

object Main:
  def main(args: Array[String]): Unit =
    val d: Door = Door.Locked("k1")
    println(d)

積型と和型の組み合わせ

実際のモデルでは、和型の中に積型を入れたり、その逆を行ったりします。各バリアントは、それぞれ独自のフィールドの積型を持てます。

case class Address(city: String, zip: String)

enum Contact:
  case Email(value: String)
  case Postal(address: Address)

object Main:
  def main(args: Array[String]): Unit =
    val c: Contact = Contact.Postal(Address("Paris", "75001"))
    println(c)

ADTのパターンマッチング

ADTはパターンマッチングで処理します。型が閉じているため、バリアントの処理漏れがあるとコンパイラーが警告します。

enum Shape:
  case Circle(r: Double)
  case Rect(w: Double, h: Double)

object Main:
  def area(s: Shape): Double = s match
    case Shape.Circle(r)  => 3.14159 * r * r
    case Shape.Rect(w, h) => w * h

  def main(args: Array[String]): Unit =
    println(area(Shape.Rect(2, 3)))

再帰的なADT

ADTは自分自身を参照できるため、木構造、リスト、式を自然にモデル化できます。

enum Expr:
  case Num(value: Int)
  case Add(left: Expr, right: Expr)

object Main:
  def eval(e: Expr): Int = e match
    case Expr.Num(v)    => v
    case Expr.Add(l, r) => eval(l) + eval(r)

  def main(args: Array[String]): Unit =
    val e = Expr.Add(Expr.Num(2), Expr.Num(3))
    println(eval(e))

Optionによるオプショナリティ

値が存在しない可能性のあるフィールドのモデル化には、nullではなくOptionを使います。型によって不在が明示され、処理も必須になります。

case class Profile(name: String, nickname: Option[String])

object Main:
  def main(args: Array[String]): Unit =
    val p = Profile("Grace", None)
    println(p.nickname.getOrElse("(none)"))

数量を正確にモデル化する

プリミティブ型への過度な依存をやめましょう。IntやStringをそのまま使うのではなく、意味のある数量をラップすることで、型が意図を示し、取り違えを防ぎます。

case class Quantity(value: Int)
case class Price(cents: Long)
case class LineItem(qty: Quantity, price: Price)

object Main:
  def main(args: Array[String]): Unit =
    val item = LineItem(Quantity(3), Price(500))
    println(item)

ADTが重要な理由

ADTによって、正しさと明確さを得られます。

  • 不正な状態を構築できません。
  • 網羅的なマッチングによって、処理漏れをコンパイル時に検出できます。
  • データの形状自体がドメインを説明します。
enum OrderStatus:
  case Pending
  case Shipped(tracking: String)
  case Delivered(at: Long)

object Main:
  def main(args: Array[String]): Unit =
    val s: OrderStatus = OrderStatus.Shipped("TRK1")
    println(s)

理解度チェック

ADTに基づくモデリングについての理解度を確認しましょう。

まとめ

ADTを使ったドメインモデリングについて学びました。

  • 積型(case class)はANDでフィールドを組み合わせます。
  • 和型(enum)はORによる選択肢を提供します。
  • 両者を入れ子にして、複雑なドメインや再帰的な構造をモデル化します。
  • nullの代わりにOptionを使います。
  • 不正な状態を表現できないように設計します。
enum Event:
  case Created(id: Int)
  case Deleted(id: Int, reason: String)

object Main:
  def describe(e: Event): String = e match
    case Event.Created(id)         => s"created $id"
    case Event.Deleted(id, reason) => s"deleted $id: $reason"

  def main(args: Array[String]): Unit =
    println(describe(Event.Deleted(1, "spam")))

よくある質問

「ADTによるモデル化」レッスンは無料ですか?

はい。「ADTによるモデル化」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Scala for Backend Engineering & Functional Programmingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Scala for Backend Engineering & Functional Programmingコースには全4レッスンが含まれています。

「ADTによるモデル化」で何を学びますか?

不正な状態を表現不可能にします ブラウザで直接実行するハンズオンコードでScala for Backend Engineering & Functional Programmingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Scala for Backend Engineering & Functional Programmingを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのScala for Backend Engineering & Functional Programmingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「ADTによるモデル化」レッスンにはどのくらい時間がかかりますか?

ほとんどの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に戻る