发布于 2026-01-06 0 阅读
0

我对 Scala 及其标准库的了解还很有限

我对 Scala 及其标准库的了解还很有限

介绍

我使用 Scala 作为主要编程语言已经三年了。最近,我出于兴趣做了一些Scala 的基础练习,结果发现了一些我以前不知道的特性和可能性。

有些东西我觉得非常实用,有些东西我则不太确定是否喜欢。在本篇博客文章中,我们将讨论……

  1. 从地图中移除元组
  2. 永无止境的通道
  3. 部分功能域
  4. 反引号的不同用法
  5. 中缀类型
  6. 萃取器

我们逐一来看吧。我没有按特定顺序排列,所以你可以随意跳过某个,或者慢慢浏览,直到找到你感兴趣的内容!

1. 从映射中移除元组

“从映射中移除元组?听起来很简单,你怎么会不知道怎么做呢?” 我同意。听起来很简单。在 Scala 中,你可以使用键值对方法从映射中移除元素-

Map(1 -> "a", 2 -> "b") - 1 == Map(2 -> "b")
Enter fullscreen mode Exit fullscreen mode

现在我们考虑一个键为二元组的映射。

Map((1, 2) -> "a", (2, 3) -> "b") - (1, 2) == Map((2, 3) -> "b")
Enter fullscreen mode Exit fullscreen mode
error: type mismatch;
 found   : Int(1)
 required: (Int, Int)
       Map((1, 2) -> 2, (2, 3) -> 3) - (1, 2)
                                        ^
error: type mismatch;
 found   : Int(2)
 required: (Int, Int)
       Map((1, 2) -> 2, (2, 3) -> 3) - (1, 2)     
                                           ^
Enter fullscreen mode Exit fullscreen mode

哎呀!这是怎么回事?原来方法不止一种-,而是两种:

def -(elem: A) = ???

def -(elem1: A, elem2: A, elems: A*) =
  this - elem1 - elem2 -- elems
Enter fullscreen mode Exit fullscreen mode

这种方法允许你一次性删除多个键,类似于`remove_keys` --,但它支持可变参数。为了使上面的示例生效,我们需要添加一对额外的括号。

Map((1, 2) -> "a", (2, 3) -> "b") - ((1, 2)) == Map((2, 3) -> "b")
Enter fullscreen mode Exit fullscreen mode

我不明白为什么会有这个方法,也不明白为什么叫它 `__getitem__`-而不是 `__getitem__`,--因为它明明可以一次性删除多个元素。但我可以肯定的是,当使用元组作为键时,它可能会导致混淆。

2. 永无止境的可穿越区域

在 Scala 中,每个集合都是一个可遍历对象Traversable。可遍历对象具有不同的操作,例如添加它们(`add` ++)、转换它们的元素(map` transform` 、`transform`、`transform`),等等。它们还提供了获取其大小信息的方法(`size` 、` flatMapsize` 、` size`)。collectisEmptynonEmptysize

当询问可遍历对象的大小时,你期望得到的答案是集合中元素的数量,对吧?List(1, 2, 3).size例如,3如果列表中有三个元素,那么答案应该是 3。但是如果是 3 个元素呢Stream.from(1).size?流(Stream)是一种可遍历对象,它的大小可能不确定。实际上,这个方法永远不会返回,它会一直遍历下去。

幸运的是,有一个名为 `issafe` 的方法hasDefiniteSize可以告诉你调用你的可遍历对象是否安全size,例如,`issafe`Stream.from(1).hasDefiniteSize会返回 `false` false。但请记住,如果此方法返回 `false` true,则集合肯定是有限的,但反过来则不能保证:

  • Stream.from(1, 2).take(5).hasDefiniteSize返回false,但是
  • Stream.from(1).take(5).size5

我并不经常使用内置Stream类型,即使使用,我也清楚它的内部结构,不会size在不合适的时候调用它。但是,如果你想提供一个接受任意类型的 API Traversable,请务必在尝试遍历到末尾之前检查它是否具有确定的大小。

3. 部分函数域

在函数式编程中,你将程序视为一系列数学函数的组合。函数是纯粹的、无副作用的输入到输出的转换。

然而,鉴于大多数编程语言中提供的标准类型(例如整数、浮点数、列表等),并非每个方法都是函数。例如,如果两个整数相除,而除数为 0,则该方法实际上未定义。

Scala 提供了一种方法来表达这一点PartialFunction,即使用 `*` 表示该函数不是全函数(从数学角度来说,这使其不再是函数而只是一个关系,因为根据定义,函数必须是全函数)。需要注意的是,Scala 并没有明确地告诉你像 `*`Int./和 `* ` 这样的方法List.head是偏函数。

你可以直接定义PartialFunction,也可以使用 case 语句定义:

val devide2 = new PartialFunction[Int, Int] {
  override def isDefinedAt(x: Int): Boolean = x != 0
  override def apply(x: Int): Int = 2 / x
}

val divide5: PartialFunction[Int, Int] = { case i if i != 0 => 5 / i }
Enter fullscreen mode Exit fullscreen mode

我之前不知道的是,isDefinedAt你需要使用这种方法来检查该函数是否可以应用于你的输入参数。

devide2.isDefinedAt(3) == true
devide5.isDefinedAt(0) == false
Enter fullscreen mode Exit fullscreen mode

当我们的函数对于输入值没有定义时,我们该如何处理这种情况?

  • 首先,尝试修正你的领域定义。如果你正在处理一个列表,并且为了head确保接收到非空列表而进行安全调用,那么请只接受非空列表类型的输入。这与我之前博文中讨论的如何选择合适的数据模型以避免无效状态密切相关。让编译器为你效劳吧!
  • 如果无法修复定义域,可以尝试修复偏函数。不要将除法定义为 `distance = 0` (Int, Int) -> Int,而是将其定义为 ` distance = 0` (Int, Int) -> Option[Int],并在除数为 0 时返回 ` None0`。这样就不再存在偏函数了。如果 `distance = 0`,head则可以使用 ` distance headOption= 0` 代替。
  • 如果你不想修改你的分部函数,你可以将它与其他分部函数组合起来,以覆盖整个定义域。你可以使用 `.` 组合两个分部函数orElse。如果第一个分部函数无法应用,Scala 将尝试使用第二个分部函数。

4. 反引号的不同用法

到目前为止,我只在需要使用保留关键字作为变量名时才使用反引号,例如在使用 Java 方法时Thread.yield。但它在处理语句时还有另一种用途case

在语句内部进行模式匹配时case,以小写字母开头的条件是局部绑定的变量名。以大写字母开头的条件则直接匹配变量名。

val A = "a"
val b = "b"

"a" match {
  case A => println("A")
  case b => println("b")
}
// prints 'A'

"b" match {
  case A => println("A")
  case b => println("b")
}
// prints 'b'

"c" match {
  case A => println("A")
  case b => println("b")
}
// prints 'b'
Enter fullscreen mode Exit fullscreen mode

在上面的例子中我们可以看到,最后一个例子中case b也匹配了"c",因为b是一个局部绑定的变量,而不是val b之前定义的。如果你想匹配,val b你可以把它重命名为val B,或者像我之前不知道的那样,把它放在 case 语句里的反引号里。

val A = "a"
val b = "b"

"b" match {
  case A => println("A")
  case `b` => println("b")
}
// prints 'b'

"c" match {
  case A => println("A")
  case `b` => println("b")
  case _ => println("_")
}
// prints '_'
Enter fullscreen mode Exit fullscreen mode

5. 中缀类型

在 Scala 中,类型参数可用于表达参数多态性,例如在泛型类中。这提供了一种抽象方法,使我们能够一次性实现适用于不同输入类型的功能。

如果你的类有多个类型参数,可以用逗号分隔它们。假设我们要实现一个类,该类保存一对任意值。

case class Pair[A, B](a: A, b: B)
Enter fullscreen mode Exit fullscreen mode

现在我们可以像这样创建新的配对:

val p: Pair[String, Int] = Pair("Frank", 28)
Enter fullscreen mode Exit fullscreen mode

但是,使用中缀类型表示法,你也可以这样写:

val p: String Pair Int = Pair("Frank", 28)
Enter fullscreen mode Exit fullscreen mode

我知道Scala的目标是成为一种可扩展、灵活且易于扩展的语言。但依我之见,如果给开发者提供太多实现或表达同一功能的方式,会使他人的代码难以阅读。

如果运气好,不同的代码风格意味着你在查看新项目的源代码时只需要适应这种风格。如果运气不好,同一个项目里就会混杂着不同的表达方式,导致代码难以阅读和理解。

我知道有些情况下中缀类型表示法很方便,但它也很容易让代码变得完全不可读。谁会想到,String ==>> Double一个用平衡二叉树实现的键值对不可变映射,居然会用这种类型呢?

6. 萃取器

为了对对象进行模式匹配,该对象必须具有一个unapply方法。具有此方法的对象称为提取器对象

定义 case 类时,编译器会自动生成一个提取器对象,以便您可以使用模式匹配。但是,也可以unapply直接定义:

object Person {
  def apply(internalId: String, name: String) = s"$internalId/$name"

  def unapply(idAndName: String): Option[String] =
    idAndName.split("/").lastOption
}

val p = Person(java.util.UUID.randomUUID.toString, "Carl")
p match {
  case Person("Carl") => println("Hi Carl!")
  case _ => println("Who are you?")
}
// prints 'Hi Carl!'
Enter fullscreen mode Exit fullscreen mode

目前一切顺利。我之前不知道的是,类的实例也可以用于提取:

class NameExtractor(prefix: String) {
  def unapply(name: String): Option[String] =
    if (name.startsWith(prefix)) Some(name) else None
}

val e = new NameExtractor("Alex")
"Alexa" match {
  case e(name) => println(s"Hi $name!")
  case _ => println("I prefer other names!")
}
Enter fullscreen mode Exit fullscreen mode

这样就可以自定义提取器对象。

结论

学习Scala 的过程非常有趣,虽然大部分内容我都已经掌握,但发现一些新知识仍然令人兴奋。我认为,即使你自认为是资深人士、专家、大师、大咖等等,也总会有你不知道的东西,时不时地温习一下基础知识总是没错的。

你对我们在这篇文章中讨论的内容有什么看法?你认为-使用可变参数的方法有用吗?你是否曾经因为没有检查无限可遍历区域的大小是否确定而尝试计算它的大小?你是否知道在调用偏函数之前需要检查它是否已定义?你的代码中经常使用反引号吗?你是否曾经定义过本应使用中缀表示法的类型?或者你是否曾经在不知不觉中使用过中缀表示法的类型?你能想到在现实世界中使用提取器类而不是对象的例子吗?

请在下方评论区告诉我你的想法!


如果你喜欢这篇文章,可以在 ko-fi 上支持我

文章来源:https://dev.to/frosnerd/what-i-did-not-know-about-scala-and-its-standard-library-401