From dece172ca39e82e4bc9f5637007e682c9b1d6a34 Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Sun, 12 Mar 2023 16:41:41 +0800 Subject: [PATCH 1/3] add code tab in _zh-cn/overviews/scala3-book/domain-modeling-fp.md --- .../scala3-book/domain-modeling-fp.md | 346 +++++++++++++++++- 1 file changed, 340 insertions(+), 6 deletions(-) diff --git a/_zh-cn/overviews/scala3-book/domain-modeling-fp.md b/_zh-cn/overviews/scala3-book/domain-modeling-fp.md index e9f6933cad..11d4779990 100644 --- a/_zh-cn/overviews/scala3-book/domain-modeling-fp.md +++ b/_zh-cn/overviews/scala3-book/domain-modeling-fp.md @@ -25,7 +25,7 @@ permalink: "/zh-cn/scala3/book/:title.html" ## 介绍 -在 FP 中,*数据* 和*对该数据的操作* 是两个独立的东西;您不必像使用 OOP 那样将它们封装在一起。 +在 FP 中,*数据*和*对该数据的操作*是两个独立的东西;您不必像使用 OOP 那样将它们封装在一起。 这个概念类似于数值代数。 当您考虑值大于或等于零的整数时,您有一*组*可能的值,如下所示: @@ -56,12 +56,47 @@ FP设计以类似的方式实现: 在 Scala 中,描述编程问题的数据模型很简单: -- 如果您想使用不同的替代方案对数据进行建模,请使用 枚举 +- 如果您想使用不同的替代方案对数据进行建模,请使用 `enum` 结构,(或者在 Scala 2 中用 `case object`)。 - 如果您只想对事物进行分组(或需要更细粒度的控制),请使用 样例类 ### 描述替代方案 -简单地由不同的选择组成的数据,如面饼大小、面饼类型和馅料,使用 Scala 3 枚举进行简洁的建模: +简单地由不同的选择组成的数据,如面饼大小、面饼类型和馅料,在 Scala 中使用枚举进行简洁的建模: + +{% tabs data_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_1 %} + +在 Scala 2 中,一个 `sealed class` 和若干个继承自该类的 `case object` 组合在一起来表示枚举: + +```scala +sealed abstract class CrustSize +object CrustSize { + case object Small extends CrustSize + case object Medium extends CrustSize + case object Large extends CrustSize +} + +sealed abstract class CrustType +object CrustType { + case object Thin extends CrustType + case object Thick extends CrustType + case object Regular extends CrustType +} + +sealed abstract class Topping +object Topping { + case object Cheese extends Topping + case object Pepperoni extends Topping + case object BlackOlives extends Topping + case object GreenOlives extends Topping + case object Onions extends Topping +} +``` + +{% endtab %} +{% tab 'Scala 3' for=data_1 %} + +在 Scala 3 中,用 `enum` 结构简洁地表示: ```scala enum CrustSize: @@ -74,6 +109,9 @@ enum Topping: case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions ``` +{% endtab %} +{% endtabs %} + > 描述不同选择的数据类型(如 `CrustSize`)有时也称为_归纳类型_。 ### 描述复合数据 @@ -81,6 +119,24 @@ enum Topping: 可以将披萨饼视为上述不同属性的_组件_容器。 我们可以使用 样例类来描述 `Pizza` 由 `crustSize`、`crustType` 和可能的多个 `Topping` 组成: +{% tabs data_2 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_2 %} + +```scala +import CrustSize._ +import CrustType._ +import Topping._ + +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) +``` + +{% endtab %} +{% tab 'Scala 3' for=data_2 %} + ```scala import CrustSize.* import CrustType.* @@ -93,6 +149,9 @@ case class Pizza( ) ``` +{% endtab %} +{% endtabs %} + > 聚合多个组件的数据类型(如`Pizza`)有时也称为_乘积类型_。 就是这样。 @@ -101,16 +160,25 @@ case class Pizza( 数据模型易于阅读,就像声明关系数据库的设计一样。 创建数据模型的值并检查它们也很容易: +{% tabs data_3 %} +{% tab 'Scala 2 and 3' for=data_3 %} + ```scala val myFavPizza = Pizza(Small, Regular, Seq(Cheese, Pepperoni)) println(myFavPizza.crustType) // prints Regular ``` +{% endtab %} +{% endtabs %} + #### 更多数据模型 我们可能会以同样的方式对整个披萨订购系统进行建模。 下面是一些用于对此类系统建模的其他 样例类: +{% tabs data_4 %} +{% tab 'Scala 2 and 3' for=data_4 %} + ```scala case class Address( street1: String, @@ -132,6 +200,9 @@ case class Order( ) ``` +{% endtab %} +{% endtabs %} + #### “瘦领域对象(贫血模型)” Debasish Ghosh 在他的《*函数式和反应式领域建模*》一书中指出,OOP 从业者将他们的类描述为封装数据和行为的“富领域模型(充血模型)”,而 FP 数据模型可以被认为是“瘦领域对象”。 @@ -144,6 +215,23 @@ Debasish Ghosh 在他的《*函数式和反应式领域建模*》一书中指出 答案实际上很简单:您只需编写对我们刚刚建模的数据值进行操作的函数(或方法)。 例如,我们可以定义一个计算披萨价格的函数。 +{% tabs data_5 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_5 %} + +```scala +def pizzaPrice(p: Pizza): Double = p match { + case Pizza(crustSize, crustType, toppings) => { + val base = 6.00 + val crust = crustPrice(crustSize, crustType) + val tops = toppings.map(toppingPrice).sum + base + crust + tops + } +} +``` + +{% endtab %} +{% tab 'Scala 3' for=data_5 %} + ```scala def pizzaPrice(p: Pizza): Double = p match case Pizza(crustSize, crustType, toppings) => @@ -153,17 +241,54 @@ def pizzaPrice(p: Pizza): Double = p match base + crust + tops ``` +{% endtab %} +{% endtabs %} + 您注意到函数的实现如何简单地遵循数据的样式:由于 `Pizza` 是一个样例类,我们使用模式匹配来提取组件并调用辅助函数来计算各个部分单独的价格。 +{% tabs data_6 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_6 %} + +```scala +def toppingPrice(t: Topping): Double = t match { + case Cheese | Onions => 0.5 + case Pepperoni | BlackOlives | GreenOlives => 0.75 +} +``` + +{% endtab %} +{% tab 'Scala 3' for=data_6 %} + ```scala def toppingPrice(t: Topping): Double = t match case Cheese | Onions => 0.5 case Pepperoni | BlackOlives | GreenOlives => 0.75 ``` +{% endtab %} +{% endtabs %} + 同样,由于 `Topping` 是一个枚举,我们使用模式匹配来区分不同的变量。 奶酪和洋葱的价格为 50ct,其余的价格为 75ct。 +{% tabs data_7 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_7 %} + +```scala +def crustPrice(s: CrustSize, t: CrustType): Double = + (s, t) match { + // if the crust size is small or medium, + // the type is not important + case (Small | Medium, _) => 0.25 + case (Large, Thin) => 0.50 + case (Large, Regular) => 0.75 + case (Large, Thick) => 1.00 + } +``` + +{% endtab %} +{% tab 'Scala 3' for=data_7 %} + ```scala def crustPrice(s: CrustSize, t: CrustType): Double = (s, t) match @@ -175,6 +300,9 @@ def crustPrice(s: CrustSize, t: CrustType): Double = case (Large, Thick) => 1.00 ``` +{% endtab %} +{% endtabs %} + 为了计算面饼的价格,我们同时对面饼的大小和类型进行模式匹配。 > 关于上面显示的所有函数的重要一点是它们是*纯函数*:它们不会改变任何数据或有其他副作用(如抛出异常或写入文件)。 @@ -229,6 +357,40 @@ Mine (Alvin, now modified, from fp-pure-functions.md): 使用这种方法,除了枚举或样例类之外,您还定义了一个包含该行为的同名伴生对象。 +{% tabs org_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=org_1 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) + +// the companion object of case class Pizza +object Pizza { + // the implementation of `pizzaPrice` from above + def price(p: Pizza): Double = ... +} + +sealed abstract class Topping + +// the companion object of enumeration Topping +object Topping { + case object Cheese extends Topping + case object Pepperoni extends Topping + case object BlackOlives extends Topping + case object GreenOlives extends Topping + case object Onions extends Topping + + // the implementation of `toppingPrice` above + def price(t: Topping): Double = ... +} +``` + +{% endtab %} +{% tab 'Scala 3' for=org_1 %} + ```scala case class Pizza( crustSize: CrustSize, @@ -252,13 +414,22 @@ object Topping: case Pepperoni | BlackOlives | GreenOlives => 0.75 ``` +{% endtab %} +{% endtabs %} + 使用这种方法,您可以创建一个 `Pizza` 并计算其价格,如下所示: +{% tabs org_2 %} +{% tab 'Scala 2 and 3' for=org_2 %} + ```scala val pizza1 = Pizza(Small, Thin, Seq(Cheese, Onions)) Pizza.price(pizza1) ``` +{% endtab %} +{% endtabs %} + 以这种方式对功能进行分组有几个优点: - 它将功能与数据相关联,让程序员(和编译器)更容易找到它。 @@ -282,6 +453,25 @@ Pizza.price(pizza1) 首先要考虑的是 `Pizza` 的“行为”。 执行此操作时,您可以像这样草拟一个 `PizzaServiceInterface` trait: +{% tabs module_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_1 %} + +```scala +trait PizzaServiceInterface { + + def price(p: Pizza): Double + + def addTopping(p: Pizza, t: Topping): Pizza + def removeAllToppings(p: Pizza): Pizza + + def updateCrustSize(p: Pizza, cs: CrustSize): Pizza + def updateCrustType(p: Pizza, ct: CrustType): Pizza +} +``` + +{% endtab %} +{% tab 'Scala 3' for=module_1 %} + ```scala trait PizzaServiceInterface: @@ -294,6 +484,9 @@ trait PizzaServiceInterface: def updateCrustType(p: Pizza, ct: CrustType): Pizza ``` +{% endtab %} +{% endtabs %} + 如图所示,每个方法都将 `Pizza` 作为输入参数——连同其他参数——然后返回一个 `Pizza` 实例作为结果 当你写一个像这样的纯接口时,你可以把它想象成一个约定,“所有扩展这个特性的非抽象类*必须*提供这些服务的实现。” @@ -301,6 +494,9 @@ trait PizzaServiceInterface: 此时您还可以做的是想象您是此 API 的使用者。 当你这样做时,它有助于草拟一些示例“消费者”代码,以确保 API 看起来像你想要的: +{% tabs module_2 %} +{% tab 'Scala 2 and 3' for=module_2 %} + ```scala val p = Pizza(Small, Thin, Seq(Cheese)) @@ -311,6 +507,9 @@ val p3 = updateCrustType(p2, Thick) val p4 = updateCrustSize(p3, Large) ``` +{% endtab %} +{% endtabs %} + 如果该代码看起来没问题,您通常会开始草拟另一个 API ——例如用于订单的 API ——但由于我们现在只关注披萨饼,我们将停止考虑接口,然后创建这个接口的具体实现。 > 请注意,这通常是一个两步过程。 @@ -322,6 +521,32 @@ val p4 = updateCrustSize(p3, Large) 现在您知道了 `PizzaServiceInterface` 的样子,您可以通过为接口中定义的所有方法体来创建它的具体实现: +{% tabs module_3 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_3 %} + +```scala +object PizzaService extends PizzaServiceInterface { + + def price(p: Pizza): Double = + ... // implementation from above + + def addTopping(p: Pizza, t: Topping): Pizza = + p.copy(toppings = p.toppings :+ t) + + def removeAllToppings(p: Pizza): Pizza = + p.copy(toppings = Seq.empty) + + def updateCrustSize(p: Pizza, cs: CrustSize): Pizza = + p.copy(crustSize = cs) + + def updateCrustType(p: Pizza, ct: CrustType): Pizza = + p.copy(crustType = ct) +} +``` + +{% endtab %} +{% tab 'Scala 3' for=module_3 %} + ```scala object PizzaService extends PizzaServiceInterface: @@ -343,10 +568,33 @@ object PizzaService extends PizzaServiceInterface: end PizzaService ``` +{% endtab %} +{% endtabs %} + 虽然创建接口和实现的两步过程并不总是必要的,但明确考虑 API 及其使用是一种好方法。 一切就绪后,您可以使用 `Pizza` 类和 `PizzaService`: +{% tabs module_4 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_4 %} + +```scala +import PizzaService._ + +val p = Pizza(Small, Thin, Seq(Cheese)) + +// use the PizzaService methods +val p1 = addTopping(p, Pepperoni) +val p2 = addTopping(p1, Onions) +val p3 = updateCrustType(p2, Thick) +val p4 = updateCrustSize(p3, Large) + +println(price(p4)) // prints 8.75 +``` + +{% endtab %} +{% tab 'Scala 3' for=module_4 %} + ```scala import PizzaService.* @@ -361,6 +609,9 @@ val p4 = updateCrustSize(p3, Large) println(price(p4)) // prints 8.75 ``` +{% endtab %} +{% endtabs %} + ### 函数对象 在 *Programming in Scala* 一书中,作者将术语“函数对象”定义为“不具有任何可变状态的对象”。 @@ -379,6 +630,38 @@ println(price(p4)) // prints 8.75 #### 例子 使用这种方法,您可以在样例类中直接实现披萨上的功能: + +{% tabs module_5 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_5 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) { + + // the operations on the data model + def price: Double = + pizzaPrice(this) // implementation from above + + def addTopping(t: Topping): Pizza = + this.copy(toppings = this.toppings :+ t) + + def removeAllToppings: Pizza = + this.copy(toppings = Seq.empty) + + def updateCrustSize(cs: CrustSize): Pizza = + this.copy(crustSize = cs) + + def updateCrustType(ct: CrustType): Pizza = + this.copy(crustType = ct) +} +``` + +{% endtab %} +{% tab 'Scala 3' for=module_5 %} + ```scala case class Pizza( crustSize: CrustSize, @@ -403,11 +686,17 @@ case class Pizza( this.copy(crustType = ct) ``` +{% endtab %} +{% endtabs %} + 请注意,与之前的方法不同,因为这些是 `Pizza` 类上的方法,它们不会将 `Pizza` 引用作为输入参数。 相反,他们用 `this` 作为当前披萨实例的引用。 现在你可以像这样使用这个新设计: +{% tabs module_6 %} +{% tab 'Scala 2 and 3' for=module_6 %} + ```scala Pizza(Small, Thin, Seq(Cheese)) .addTopping(Pepperoni) @@ -415,6 +704,9 @@ Pizza(Small, Thin, Seq(Cheese)) .price ``` +{% endtab %} +{% endtabs %} + ### 扩展方法 最后,我们展示了一种介于第一个(在伴生对象中定义函数)和最后一个(将函数定义为类型本身的方法)之间的方法。 @@ -428,6 +720,9 @@ Pizza(Small, Thin, Seq(Cheese)) 让我们再次回顾一下我们的例子。 +{% tabs module_7 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_7 %} + ```scala case class Pizza( crustSize: CrustSize, @@ -435,7 +730,7 @@ case class Pizza( toppings: Seq[Topping] ) -extension (p: Pizza) +implicit class PizzaOps(p: Pizza) { def price: Double = pizzaPrice(p) // implementation from above @@ -450,12 +745,48 @@ extension (p: Pizza) def updateCrustType(ct: CrustType): Pizza = p.copy(crustType = ct) +} ``` +在上面的代码中,我们将披萨上的不同方法定义为_implicit class_。 +用 `implicit class PizzaOps(p: Pizza)`,不管什么时候导入 `PizzaOps`,它的方法在 `Pizza` 的实例上都是可用的。 +在本例中接收者是 `p`。 + +{% endtab %} +{% tab 'Scala 3' for=module_7 %} +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) + +extension (p: Pizza) + def price: Double = + pizzaPrice(p) // implementation from above + + def addTopping(t: Topping): Pizza = + p.copy(toppings = p.toppings :+ t) + + def removeAllToppings: Pizza = + p.copy(toppings = Seq.empty) + + def updateCrustSize(cs: CrustSize): Pizza = + p.copy(crustSize = cs) + + def updateCrustType(ct: CrustType): Pizza = + p.copy(crustType = ct) +``` 在上面的代码中,我们将披萨上的不同方法定义为_扩展方法_。 -对于 `extension (p: Pizza)`,我们想在 `Pizza` 的实例上让方法可用,并在下文中把扩展的实例称为 `p`。 +对于 `extension (p: Pizza)`,我们想在 `Pizza` 的实例上让方法可用。在本例中接收者是 `p`。 + +{% endtab %} +{% endtabs %} -这样我们就可以获得和之前一样的 API,同时能够在任何其他模块中定义扩展。 +使用扩展方法,我们就可以获得和之前一样的 API: + +{% tabs module_8 %} +{% tab 'Scala 2 and 3' for=module_8 %} ```scala Pizza(Small, Thin, Seq(Cheese)) @@ -464,6 +795,9 @@ Pizza(Small, Thin, Seq(Cheese)) .price ``` +{% endtab %} +{% endtabs %} + 通常,如果您是数据模型的设计者,您将在伴生对象中定义您的扩展方法。 这样,它们已经可供所有用户使用。 否则,扩展方法需要显式导入才能使用。 From fda45ca4d6d0f16cd781c9e607205090cb8ac6c0 Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Wed, 15 Mar 2023 22:44:14 +0800 Subject: [PATCH 2/3] Update _zh-cn/overviews/scala3-book/domain-modeling-fp.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 梦境迷离 <568845948@qq.com> --- _zh-cn/overviews/scala3-book/domain-modeling-fp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_zh-cn/overviews/scala3-book/domain-modeling-fp.md b/_zh-cn/overviews/scala3-book/domain-modeling-fp.md index 11d4779990..960eafddf4 100644 --- a/_zh-cn/overviews/scala3-book/domain-modeling-fp.md +++ b/_zh-cn/overviews/scala3-book/domain-modeling-fp.md @@ -783,7 +783,7 @@ extension (p: Pizza) {% endtab %} {% endtabs %} -使用扩展方法,我们就可以获得和之前一样的 API: +使用扩展方法,我们可以获得和之前一样的 API,同时也能够在任何其他模块中定义扩展: {% tabs module_8 %} {% tab 'Scala 2 and 3' for=module_8 %} From b82fc75d7b65649f71e239ad71f204e010bf60bc Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 17 Mar 2023 09:33:27 +0800 Subject: [PATCH 3/3] corrected. --- _zh-cn/overviews/scala3-book/domain-modeling-fp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_zh-cn/overviews/scala3-book/domain-modeling-fp.md b/_zh-cn/overviews/scala3-book/domain-modeling-fp.md index 960eafddf4..2e500de059 100644 --- a/_zh-cn/overviews/scala3-book/domain-modeling-fp.md +++ b/_zh-cn/overviews/scala3-book/domain-modeling-fp.md @@ -783,7 +783,7 @@ extension (p: Pizza) {% endtab %} {% endtabs %} -使用扩展方法,我们可以获得和之前一样的 API,同时也能够在任何其他模块中定义扩展: +使用扩展方法,我们可以获得和之前一样的 API: {% tabs module_8 %} {% tab 'Scala 2 and 3' for=module_8 %}