Scala for Backend Engineering & Functional Programming · 课时

使用代数数据类型建模

让非法状态无法表示

第 1 / 4 课13 个步骤

使用代数数据类型建模 是 CoddyKit 上的免费 Scala for Backend Engineering & Functional Programming 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 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)

和类型

和类型是一个值,它恰好属于多个变体中的一种。Scala 3 的枚举可以直接表达这一点。

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)

糟糕的设计与良好的设计

带有可空字段的扁平记录容易产生非法状态。将每种情况建模为一个变体即可消除这些状态。

  • 糟糕的设计: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。由于类型是封闭的,如果遗漏某个变体,编译器会发出警告。

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 可以引用自身,从而自然地建模树、列表和表达式。

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 表示可选性

使用 Option 代替 null,表示某个字段可能不存在。该类型会明确表示缺失情况,并强制您进行处理。

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 提供多种选择。
  • 嵌套使用它们,以建模丰富的领域和递归结构。
  • 使用 Option 代替 null。
  • 进行设计,使非法状态无法表示。
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")))
免费开始

用 AI 导师学习 Scala — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
39
课程
143

常见问题解答

「使用代数数据类型建模」课时是免费的吗?

是的 — 「使用代数数据类型建模」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Scala for Backend Engineering & Functional Programming 课程的其余内容,请升级到 CoddyKit PRO。 Scala for Backend Engineering & Functional Programming 课程共包含 4 节课。

「使用代数数据类型建模」这节课中我会学到什么?

让非法状态无法表示 你通过在浏览器中直接运行的动手代码来练习 Scala for Backend Engineering & Functional Programming,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Scala for Backend Engineering & Functional Programming 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Scala for Backend Engineering & Functional Programming 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「使用代数数据类型建模」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Scala for Backend Engineering & Functional Programming 课中编写并运行代码吗?

能。每节 Scala for Backend Engineering & Functional Programming 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用代数数据类型建模
  2. 智能构造器
  3. 新类型
  4. 组合领域模型
← 返回 Scala for Backend Engineering & Functional Programming