【问题标题】:Scala: `ambigious implicit values` but the right value is not event foundScala:“模糊的隐含值”,但未找到正确的值
【发布时间】:2020-12-21 04:12:43
【问题描述】:

我正在编写一个小型 Scala 程序,它应该:

  1. 从本地 FS 读取文件(逐行)
  2. 从每一行解析三个双精度值
  3. 根据这三个值创建案例类的实例
  4. 将这些实例传递给二进制堆

为了能够将Strings 解析为Doubles 和CoordinatePoints,我想出了这个特征:

trait Parseable[T] {
  def parse(input: String): Either[String, T]
}

我有许多类型对象的实现:

object Parseable {
  implicit val parseDouble: Parseable[Double] = new Parseable[Double] {
    override def parse(input: String): Either[String, Double] = {
      val simplifiedInput = input.replaceAll("[ \\n]", "").toLowerCase
      try Right(simplifiedInput.toDouble) catch {
        case _: NumberFormatException =>
          Left(input)
      }
    }
  }

  implicit val parseInt: Parseable[Int] = new Parseable[Int] {
    override def parse(input: String): Either[String, Int] = {
      val simplifiedInput = input.replaceAll("[ \\n]", "").toLowerCase
      try Right(simplifiedInput.toInt) catch {
        case _: NumberFormatException =>
          Left(input)
      }
    }
  }

  implicit val parseCoordinatePoint: Parseable[CoordinatePoint] = new Parseable[CoordinatePoint] {
    override def parse(input: String): Either[String, CoordinatePoint] = {
      val simplifiedInput = input.replaceAll("[ \\n]", "").toLowerCase
      val unparsedPoints: List[String] = simplifiedInput.split(",").toList
      val eithers: List[Either[String, Double]] = unparsedPoints.map(parseDouble.parse)
      val sequence: Either[String, List[Double]] = eithers.sequence
      sequence match {
        case Left(value) => Left(value)
        case Right(doublePoints) => Right(CoordinatePoint(doublePoints.head, doublePoints(1), doublePoints(2)))
      }
    }
  }
}

我有一个通用对象,它将调用委托给相应的隐式Parseable(在同一个文件中):

object InputParser {
  def parse[T](input: String)(implicit p: Parseable[T]): Either[String, T] = p.parse(input)
}

仅供参考 - 这是CoordinatePoint 案例类:

case class CoordinatePoint(x: Double, y: Double, z: Double)

在我的主程序中(在验证文件存在且不为空等之后)我想将每一行转换为CoordinatePoint 的实例,如下所示:

  import Parseable._
  import CoordinatePoint._

  ...
  private val bufferedReader = new BufferedReader(new FileReader(fileName))

  private val streamOfMaybeCoordinatePoints: Stream[Either[String, CoordinatePoint]] = Stream
    .continually(bufferedReader.readLine())
    .takeWhile(_ != null)
    .map(InputParser.parse(_))

我得到的错误是:

[error] /home/vgorcinschi/data/eclipseProjects/Algorithms/Chapter 2 Sorting/algorithms2_1/src/main/scala/ca/vgorcinschi/algorithms2_4/selectionfilter/SelectionFilter.scala:42:27: ambiguous implicit values:
[error]  both value parseDouble in object Parseable of type => ca.vgorcinschi.algorithms2_4.selectionfilter.Parseable[Double]
[error]  and value parseInt in object Parseable of type => ca.vgorcinschi.algorithms2_4.selectionfilter.Parseable[Int]
[error]  match expected type ca.vgorcinschi.algorithms2_4.selectionfilter.Parseable[T]
[error]     .map(InputParser.parse(_))
[error]                           ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
[error] Total time: 1 s, completed Sep 1, 2020 10:38:18 PM

我不明白也不知道在哪里寻找为什么编译器会找到 Parseable[Int]Parseable[Double] 但不是唯一正确的 - Parseable[CoordinatePoint]

所以我想,好吧,让我通过预先指定转换函数来帮助编译器:

  private val bufferedReader = new BufferedReader(new FileReader(fileName))

  val stringTransformer: String => Either[String, CoordinatePoint] = s => InputParser.parse(s)

  private val streamOfMaybeCoordinatePoints: Stream[Either[String, CoordinatePoint]] = Stream
    .continually(bufferedReader.readLine())
    .takeWhile(_ != null)
    .map(stringTransformer)

唉,这会产生相同的错误,只是在代码上方 - 在函数声明中。

我很想知道是什么导致了这种行为。既是为了纠正代码,也是为了个人知识。在这一点上我很好奇。

【问题讨论】:

  • 所以应该是Parseable.parseCoordinatePoint.parse 我试过了,我得到了这个[error] there was one unchecked warning; re-run with -unchecked for details [error] there were three feature warnings; re-run with -feature for details [error] two errors found [error] (Compile / compileIncremental) Compilation failed [error] Total time: 3 s, completed Sep 1, 2020 11:00:26 PM 可能不相关。正如你所说 - 这不是隐含的优势,以便编译器选择正确的值吗?
  • 尝试map(InputParser.parse[CoordinatePoint]) 你必须告诉编译器你想要哪种类型,它会搜索该类型的隐式。 - 顺便说一句,我建议你使用 scala.util.Usingscala.io.Source 来读取文件。
  • @JOHN 擦除不会以任何方式影响编译器。擦除是其中一个运行时的结果,而不是语言的属性。
  • @JOHN 当然会被编写JVM字节码的编译器考虑。根据这个论点,JVM 字节码是语言的一部分,或者装箱和拆箱是语言的一部分,或者由 scalajs 编译器完成的缩小也是语言的一部分。 - 同样,擦除不是语言概念,在运行类型检查器和隐式解析时不考虑它。但要晚得多。虽然擦除对于 Scala 程序员来说是一个重要的概念,但由于我们的大部分代码都在 JVM 中运行,因此我们需要将语言与 (默认) 运行时分开。
  • @LuisMiguelMejíaSuárez 你说得对,在 dotty 中编译 scastie.scala-lang.org/LFznJGiPRKyGhfwvfxyD5Q(也在 0.28.0-bin-20200901-0d22c74-NIGHTLY 中测试过)。

标签: scala typeclass implicit either


【解决方案1】:

一种解决方法是明确指定类型参数

InputParser.parse[CoordinatePoint](_)

另一个是优先考虑隐式。例如

trait LowPriorityParseable1 {
  implicit val parseInt: Parseable[Int] = ...
}

trait LowPriorityParseable extends LowPriorityParseable1 {
  implicit val parseDouble: Parseable[Double] = ...
}

object Parseable extends LowPriorityParseable {
  implicit val parseCoordinatePoint: Parseable[CoordinatePoint] = ...
}

顺便说一句,由于您将隐式放入伴随对象中,现在导入它们没有多大意义。

在调用站点

object InputParser {
  def parse[T](input: String)(implicit p: Parseable[T]): Either[String, T] = p.parse(input)
}

类型参数T被推断(如果没有明确指定)不是在隐式被解析(类型推断和隐式解析相互影响)。否则下面的代码将无法编译

trait TC[A]
object TC {
  implicit val theOnlyImplicit: TC[Int] = null
}    
def materializeTC[A]()(implicit tc: TC[A]): TC[A] = tc
  
materializeTC() // compiles, A is inferred as Int

因此,在隐式解析期间,编译器不会过早地推断类型(否则在TC 的示例中,类型A 将被推断为Nothing 并且不会找到隐式)。顺便说一句,一个例外是隐式转换,编译器会尝试急切地推断类型(sometimes 这也会带来麻烦)

// try to infer implicit parameters immediately in order to:
//   1) guide type inference for implicit views
//   2) discard ineligible views right away instead of risking spurious ambiguous implicits

https://github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/nsc/typechecker/Implicits.scala#L842-L854

【讨论】:

  • 谢谢@Dmytro。很有见地!
【解决方案2】:

编译器在尝试查找第二个参数列表中的隐式之前没有推断和修复.map(InputParser.parse(_))中的类型参数T的问题。

在编译器中,有一个具体的算法可以根据自己的逻辑、约束和权衡来推断类型。在您使用它的具体编译器版本中,它首先进入参数列表并逐个列表推断和检查类型,并且仅在最后,它通过返回类型推断类型参数(我并不暗示在其他版本中它有所不同,我只是指出它是实现行为而不是基本约束)。

更准确地说,在第二个参数列表的类型检查步骤中,类型参数T 没有被推断或指定。 T(当时)是存在的,它可以是任何/每种类型,并且有 3 种不同的隐式对象适合这种类型。

这就是编译器及其类型推断目前的工作方式。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-04-12
    • 1970-01-01
    • 2015-11-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多