0
votes

I am writing a small Scala Program which should:

  1. Read a file (line by line) from a local FS
  2. Parse from each line three double values
  3. Make instances of a case class based on those three values
  4. Pass those instances to a Binary Heap

To be able to parse Strings to both Doubles and CoordinatePoints I've came up with this trait:

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

and I have a number of type object implementations for the latter:

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)))
      }
    }
  }
}

I have a common object that delegates the call to a corresponding implicit Parseable (in the same file):

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

and just for reference - this is the CoordinatePoint case class:

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

In my main program (after having validated that the file is there, and is not empty, etc..) I want to transform each line into an instance of CoordinatePoint as follows:

  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(_))

and the error I get is this:

[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

I don't understand nor know where to look for why is the compiler finding Parseable[Int] and Parseable[Double] but not the only right one - Parseable[CoordinatePoint].

So I thought, ok let me give the compiler a hand by specifying the transformation function from beforehand:

  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)

Alas this yields the same error just a bit up the code - in the function declaration.

I would love to learn what is that that causes such behavior. Both to rectify the code and for personal knowledge. At this point I am very curious.

2
So it would be Parseable.parseCoordinatePoint.parse I tried it and I get this [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 Probably unrelated. To what you said - isn't it the advantage of implicits so that the compiler picks the right value?vasigorc
Try map(InputParser.parse[CoordinatePoint]) you have to tell the compiler which type do you want and it will search the implicit for that type. - BTW, I would recommend you to use scala.util.Using & scala.io.Source for reading the file.Luis Miguel Mejía Suárez
@JOHN erasure do not affect the compiler in any way. Erasure is a consequence of one of the runtimes, not a property of the language.Luis Miguel Mejía Suárez
@JOHN Of course it will be considered by the compiler that writes JVM byte code. By that argument then the JVM byte code is part of the language, or the boxing and unboxing are part of the language, or the minification done by the scalajs compiler is also part of the language. - Again, erasure is not a language concept, it is not considered when the type checker and the implicit resolution is run. But much latter. While erasure is an important concept for Scala programmers, since most of our code runs in the JVM, we need to separate the language from the (default) runtime.Luis Miguel Mejía Suárez
@LuisMiguelMejíaSuárez You're right, in dotty this compiles scastie.scala-lang.org/LFznJGiPRKyGhfwvfxyD5Q (tested in 0.28.0-bin-20200901-0d22c74-NIGHTLY too).Dmytro Mitin

2 Answers

3
votes

One fix is to specify type prameter explicitly

InputParser.parse[CoordinatePoint](_)

Another is to prioritize implicits. For example

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] = ...
}

By the way, since you put implicits into the companion object it doesn't make much sense now to import them.

In the call site of

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

type parameter T is inferred (if not specified explicitly) not before the implicit is resolved (type inference and implicit resolution make impact on each other). Otherwise the following code wouldn't compile

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

So during implicit resolution compiler tries to infer types not too early (otherwise in the example with TC type A would be inferred as Nothing and implicit wouldn't be found). By the way, an exception is implicit conversions where compiler tries to infer types eagerly (sometimes this can make troubles too)

// 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

1
votes

The problem that the compiler does not inference and fix type parameter T in .map(InputParser.parse(_)) before trying to find the implicit in the second parameter list.

In the compiler, there is a concrete algorithm that infers types with its own logic, constraints, and tradeoffs. In that concrete compiler version that you use it first goes to the parameter lists and infer and checks types list by list, and only at the end, it infers type parameter by returning type (I do not imply that in other versions it differs, I only point out that it is implementation behavior not a fundamental constraint).

More precisely what is going on is that type parameter T is not being inferred or specified somehow at the step of typechecking of the second parameter list. T (at that point) is existential and it can be any/every type and there is 3 different implicit object that suitable for such type.

It is just how the compiler and its type inference works for now.