Scala学习函数式编程续 case类
Posted 顧棟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Scala学习函数式编程续 case类相关的知识,希望对你有一定的参考价值。
CASE CLASSES
https://docs.scala-lang.org/overviews/scala-book/case-classes.html
另一个为函数式编程提供支持的 Scala 特性是 case 类。案例类具有常规类的所有功能,等等。当编译器在类前面看到 case 关键字时,它会为您生成代码,具有以下好处:
- 默认情况下,案例类构造函数参数是公共 val 字段,因此为每个参数生成访问器方法。
- apply 方法是在类的伴生对象中创建的,因此您不需要使用 new 关键字来创建类的新实例。
- 生成了一个 unapply 方法,它使您可以在匹配表达式中以更多方式使用案例类。
- 在类中生成了一个复制方法。你可能不会在 Scala/OOP 代码中使用这个特性,但它在 Scala/FP 中一直在使用。
- 生成了 equals 和 hashCode 方法,它们让您可以比较对象并轻松地将它们用作映射中的键。
- 生成了默认的 toString 方法,有助于调试。
这些功能都在以下部分中进行了演示。
With apply
you don’t need new
当你将一个类定义为一个 case
类时,你不必使用 new
关键字来创建一个新的实例:
scala> case class Person(name: String, relation: String)
defined class Person
// "new" not needed before Person
scala> val christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)
正如在上一课中所讨论的,这是因为在 Person 的伴生对象中生成了一个名为“apply”的方法。
No mutator methods
默认情况下,案例类构造函数参数是 val
字段,因此为每个参数生成一个 accessor 方法:
scala> christina.name
res0: String = Christina
但是,不会生成 mutator 方法:
// can't mutate the `name` field
scala> christina.name = "Fred"
<console>:10: error: reassignment to val
christina.name = "Fred"
^
因为在 FP 中你永远不会改变数据结构,所以构造函数字段默认为 val
是有道理的。
An unapply
method
在上一课关于伴随对象的课程中,您看到了如何编写 unapply
方法。 case 类的一个好处是它会自动为你的类生成一个 unapply
方法,所以你不必编写一个。
为了证明这一点,想象一下你有这个特征:
trait Person {
def name: String
}
然后,创建这些案例类来扩展该特征:
case class Student(name: String, year: Int) extends Person
case class Teacher(name: String, specialty: String) extends Person
因为它们被定义为 case 类——并且它们具有内置的 unapply
方法——你可以编写这样的匹配表达式:
def getPrintableString(p: Person): String = p match {
case Student(name, year) =>
s"$name is a student in Year $year."
case Teacher(name, whatTheyTeach) =>
s"$name teaches $whatTheyTeach."
}
注意 case
语句中的这两种模式:
case Student(name, year) =>
case Teacher(name, whatTheyTeach) =>
这些模式之所以有效,是因为 Student
和 Teacher
被定义为具有类型签名符合特定标准的 unapply
方法的案例类。 从技术上讲,这些示例中显示的特定类型的模式匹配称为构造函数模式。
Scala 标准是
unapply
方法返回包含在Option
中的元组中的case 类构造函数字段。 解决方案的“元组”部分已在上一课中展示。
为了展示该代码是如何工作的,创建一个 Student
和 Teacher
的实例:
val s = Student("Al", 1)
val t = Teacher("Bob Donnan", "Mathematics")
接下来,当您使用这两个实例调用 getPrintableString
时,REPL 中的输出如下所示:
scala> getPrintableString(s)
res0: String = Al is a student in Year 1.
scala> getPrintableString(t)
res1: String = Bob Donnan teaches Mathematics.
关于
unapply
方法和提取器的所有内容对于这样的介绍性书籍来说有点高级,但是因为案例类是一个重要的FP主题,所以最好覆盖它们,而不是跳过它们。
copy
method
case
类还有一个自动生成的 copy
方法,当你需要执行 a) 克隆对象和 b) 在克隆过程中更新一个或多个字段的过程时,它非常有用。 例如,这就是 REPL 中的流程:
scala> case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
defined class BaseballTeam
scala> val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
cubs1908: BaseballTeam = BaseballTeam(Chicago Cubs,1908)
scala> val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
如图所示,当您使用 copy
方法时,您所要做的就是提供要在克隆过程中修改的字段的名称。
因为您从不改变 FP 中的数据结构,这就是您从现有实例创建类的新实例的方式。 此过程可称为“复制时更新”。
equals
and hashCode
methods
Case 类也有自动生成的 equals
和 hashCode
方法,因此可以比较实例:
scala> case class Person(name: String, relation: String)
defined class Person
scala> val christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)
scala> val hannah = Person("Hannah", "niece")
hannah: Person = Person(Hannah,niece)
scala> christina == hannah
res1: Boolean = false
这些方法还让您可以轻松地在集合和地图等集合中使用您的对象。
toString
methods
最后,case
类还有一个很好的默认 toString
方法实现,这至少在调试代码时很有帮助:
scala> christina
res0: Person = Person(Christina,niece)
The biggest advantage
虽然所有这些特性都对函数式编程有很大好处,但正如他们在书中所写,Scala 编程 (Odersky、Spoon 和 Venners),“案例类的最大优势是它们支持模式匹配。” 模式匹配是 FP 语言的一大特性,Scala 的 case 类提供了一种在匹配表达式等领域实现模式匹配的简单方法。
CASE OBJECTS
https://docs.scala-lang.org/overviews/scala-book/case-objects.html
在我们进入case对象之前,我们应该提供一些关于“常规”Scala对象的背景知识。 正如我们在本书前面提到的,当你想创建一个单例对象时,你可以使用 Scala 的“对象”。 正如文档所述,“与类的单个实例无关的方法和值属于单例对象,表示为 使用关键字object
而不是class
。”
一个常见的例子是当你创建一个“工具”对象时,比如这个:
object PizzaUtils {
def addTopping(p: Pizza, t: Topping): Pizza = ...
def removeTopping(p: Pizza, t: Topping): Pizza = ...
def removeAllToppings(p: Pizza): Pizza = ...
}
Or this one:
object FileUtils {
def readTextFileAsString(filename: String): Try[String] = ...
def copyFile(srcFile: File, destFile: File): Try[Boolean] = ...
def readFileToByteArray(file: File): Try[Array[Byte]] = ...
def readFileToString(file: File): Try[String] = ...
def readFileToString(file: File, encoding: String): Try[String] = ...
def readLines(file: File, encoding: String): Try[List[String]] = ...
}
这是使用 Scala object
结构的常用方法。
Case objects
case object
就像一个 object
,但就像一个 case 类比一个普通类具有更多的特征一样,一个 case 对象比一个普通对象具有更多的特征。 其特点包括:
- 它是可序列化的
- 它有一个默认的
hashCode
实现 - 它有一个改进的
toString
实现
由于这些特性,case 对象主要用于两个地方(而不是常规对象):
- 创建枚举时
- 为要在其他对象之间传递的“消息”创建容器时 (such as with the Akka actors library)
Creating enumerations with case objects
正如我们在本书前面所展示的,您可以像这样在 Scala 中创建枚举:
sealed trait Topping
case object Cheese extends Topping
case object Pepperoni extends Topping
case object Sausage extends Topping
case object Mushrooms extends Topping
case object Onions extends Topping
sealed trait CrustSize
case object SmallCrustSize extends CrustSize
case object MediumCrustSize extends CrustSize
case object LargeCrustSize extends CrustSize
sealed trait CrustType
case object RegularCrustType extends CrustType
case object ThinCrustType extends CrustType
case object ThickCrustType extends CrustType
然后在您的代码中稍后使用这些枚举:
case class Pizza (
crustSize: CrustSize,
crustType: CrustType,
toppings: Seq[Topping]
)
Using case objects as messages
案例对象派上用场的另一个地方是当您想对“消息”的概念进行建模时。 例如,假设您正在编写一个类似于 Amazon 的 Alexa 的应用程序,并且您希望能够传递“说话”消息,例如“说出所附的文本”、“停止说话”、“暂停”和“继续” 。” 在 Scala 中,您可以为这些消息创建单例对象,如下所示:
case class StartSpeakingMessage(textToSpeak: String)
case object StopSpeakingMessage
case object PauseSpeakingMessage
case object ResumeSpeakingMessage
请注意,StartSpeakingMessage
被定义为一个 case class 而不是 case object。 这是因为 case 对象不能有任何构造函数参数。
鉴于这些消息,如果 Alexa 是使用 Akka 库编写的,您会在“speak”类中找到这样的代码:
class Speak extends Actor {
def receive = {
case StartSpeakingMessage(textToSpeak) =>
// code to speak the text
case StopSpeakingMessage =>
// code to stop speaking
case PauseSpeakingMessage =>
// code to pause speaking
case ResumeSpeakingMessage =>
// code to resume speaking
}
}
这是在 Scala 应用程序中传递消息的一种很好的、安全的方式。
FUNCTIONAL ERROR HANDLING IN SCALA
https://docs.scala-lang.org/overviews/scala-book/functional-error-handling.html
因为函数式编程就像代数,所以没有空值或异常。 但是当然,当您尝试访问已关闭的服务器或丢失的文件时,您仍然会遇到异常,那么您该怎么办? 本课演示了 Scala 中函数式错误处理的技术。
Option/Some/None
我们已经演示了在 Scala 中处理错误的一种技术:名为Option
、Some
和None
的三个类。 不是编写像 toInt
这样的方法来抛出异常或返回空值,而是声明该方法返回一个 Option
,在这种情况下是一个 Option[Int]
:
def toInt(s: String): Option[Int] = {
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
}
稍后在您的代码中,您使用 match
和 for
表达式处理来自 toInt
的结果:
toInt(x) match {
case Some(i) => println(i)
case None => println("That didn't work.")
}
val y = for {
a <- toInt(stringA)
b <- toInt(stringB)
c <- toInt(stringC)
} yield a + b + c
这些方法在 “No Null Values”课程中讨论过,所以我们不会在这里重复讨论。
Try/Success/Failure
另一个名为 Try
、Success
和 Failure
的类的工作方式与 Option
、Some
和 None
类似,但具有两个不错的特性:
Try
使得捕获异常变得非常简单Failure
包含异常
这是为使用这些类而重新编写的 toInt
方法。 首先,将类导入当前范围:
import scala.util.{Try,Success,Failure}
之后,这就是 toInt
和 Try
的样子:
def toInt(s: String): Try[Int] = Try {
Integer.parseInt(s.trim)
}
如您所见,这比 Option/Some/None 方法要短得多,并且可以进一步缩短为:
def toInt(s: String): Try[Int] = Try(Integer.parseInt(s.trim))
这两种方法都比 Option/Some/None 方法短得多。
REPL 演示了这是如何工作的。 一、成功案例:
scala> val a = toInt("1")
a: scala.util.Try[Int] = Success(1)
其次,这是当 Integer.parseInt
抛出异常时的样子:
scala> val b = toInt("boo")
b: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "boo")
正如该输出所示,toInt
返回的 Failure
包含失败的原因,即异常。
有很多方法可以处理 Try
的结果——包括从失败中“recover”的能力——但常见的方法仍然涉及使用 match
和 for
表达式:
toInt(x) match {
case Success(i) => println(i)
case Failure(s) => println(s"Failed. Reason: $s")
}
val y = for {
a <- toInt(stringA)
b <- toInt(stringB)
c <- toInt(stringC)
} yield a + b + c
请注意,当使用 for 表达式并且一切正常时,它返回包含在 Success
中的值:
scala.util.Try[Int] = Success(6)
相反,如果失败,则返回一个 Failure
:
scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "a")
Even more …
还有其他类的工作方式类似,包括Scala库中的Either/Left/Right等第三方库,但常用Option/Some/None和Try/Success/Failure,先学好 .
你可以使用任何你喜欢的东西,但 Try/Success/Failure 通常用于处理可能抛出异常的代码——因为你几乎总是想了解异常——而 Option/Some/None 用于其他地方,例如 避免使用空值。
以上是关于Scala学习函数式编程续 case类的主要内容,如果未能解决你的问题,请参考以下文章