1

I am new in Scala and I am referring book "Scala for Impatient - Second Edition".

I am working on a small code where I have created a class Fraction which as two Int fields num (number) and den(denominator). The class has a method * which performs multiplication of num and den and returns new Fraction instance.

Here, for understanding the working of implicit, I have created an object FractionConversions which helps with two implicit method intToFraction and fractionToDouble.

While testing the code in ImplicitConversionTester, I have imported the FractionConversions._ path, so both of the implicit methods are available to compiler.

Now, refer the code to make the picture more clear.

package OperatorOverloadingAndImplicitConversion
    
    class Fraction(n : Int, d : Int) {
      private val num = this.n
      private val den = this.d
    
      def *(other : Fraction) = new Fraction(num*other.num, den*other.den)
    
      override def toString: String = {
        s"Value of Fraction is : ${(num*0.1)/den}"
      }
    }
    
    object Fraction {
      def apply(n: Int, d: Int): Fraction = new Fraction(n, d)
    }
    
    object FractionConversions {
    
      implicit def fractionToDouble(f : Fraction): Double = {
        println("##### FractionConversions.fractionToDouble called ...")
       (f.num*0.1)/f.den
      }
    
      implicit def intToFraction(n : Int) : Fraction = {
        println("##### FractionConversions.intToFraction called ...")
        new Fraction(n,1)
      }
    }
    
    
    object ImplicitConversionTester extends App {
    
      import FractionConversions._
    
      /*
       * CASE 1 : Here,  "fractionToDouble" implicit is called. 
Why "intToFraction" was eligible but not called ? 
    
       */
      val a = 2 * Fraction(1,2)
    
      /*
       * CASE 2 : Works as expected. 
Here, number "2" is converted to Fraction using "intToFraction" implicit.
       */
      val b = 2.den
    
      /*
       * CASE 3: Why again "intToFraction" but not "fractionToDouble" ? Why ?
       */
      val c = Fraction(4,5) * 3
      println(a) // output : 0.1 FractionConversions.fractionToDouble called
      println(b) // output : 1 FractionConversions.intToFraction called
      println(c) // output : Value of Fraction is : 0.24000000000000005. FractionConversions.intToFraction called
    }

I have query in above code :

  • Case#1 : For statement val a = 2 * Fraction(1,2), Why fractionToDouble implicit is getting called here even though intToFraction is also eligible in this case ?

  • case#3 : For statement val c = Fraction(4,5) * 3, why intToFraction called ? Why fractionToDouble was not used ?

Here, I tried to replicate the following scenario mentioned in the book, so above question arises. enter image description here

So, should We summarise that, the compiler avoids converting the left side operand and select the right side to convert if both of them are eligible for conversion ? For example, In case of (a*b), a is always ignored and b is converted to expected type even though a and b both are eligible for conversion ?

Gunjan Shah
  • 5,088
  • 16
  • 53
  • 72
  • 4
    Stay away from implicit conversions whenever possible, they lead to confusing code and can make the compiler generate slower or wrong code. Rather prefer two extension methods: `toFraction` on `Int` and `toDouble` on `Fraction` – Luis Miguel Mejía Suárez Aug 22 '21 at 17:47
  • 1
    @LuisMiguelMejíaSuárez Why should `toDouble` be an extension method if the OP owns the code of `Fraction`? Also, `implicit class IntOps(n: Int) { def toFraction: Fraction = new Fraction(n,1) }` is just a sugar for `class IntOps(n: Int) { def toFraction: Fraction = new Fraction(n,1) }; implicit def intToOps(n: Int): IntOps = new IntOps(n)`, so you actually can't stay absolutely away from implicit conversions. – Dmytro Mitin Aug 22 '21 at 19:08
  • @DmytroMitin yeah the first was mostly bad wording on my side, and yes while technically speaking extension methods are implemented using implicit conversions under the hood, both are different patterns and you know that. Is like saying that one can not stay away of go-to because `if` and `while` are implemented at the byte code as those, or that you can't stay away from loops because `map` is implemented using those, or that you can't stay away of machine code because everything will be executed by a CPU. – Luis Miguel Mejía Suárez Aug 22 '21 at 19:39
  • @LuisMiguelMejíaSuárez I just wanted to emphasize that with extension methods we don't avoid implicit conversions, we just hide them under the hood. From "implicit conversions... can make the compiler generate slower or wrong code" somebody could think the contrary. Yes, in presence of implicit conversions type inference/type checking/implicit resolution can be trickier but the same is true for extension methods/implicit classes in Scala 2 because they are desugared into those. – Dmytro Mitin Aug 22 '21 at 20:16
  • 2
    @DmytroMitin I understand and totally agree, but my point still stands; while you are technically correct, practice says otherwise. Since extension methods require the explicit invocation of the extension method the compiler search is faster and safer than a general conversion from two unrelated types. In any case, this is not my point alone, a similar point can be found in the stdlib docs of previous **Scala** versions where the `scala.collection.JavaConversions` object still existed but was deprecated: https://scala-lang.org/files/archive/api/2.12.13/scala/collection/JavaConversions$.html – Luis Miguel Mejía Suárez Aug 22 '21 at 20:31
  • @LuisMiguelMejíaSuárez It will be helpful if you can post some example in answer to demonstrate the solution of ambiguity implicit problem. I am referring one blog here to understand the how to explicitly select implicit method : https://www.wix.engineering/post/scala-extension-methods-via-implicit-classes . But, I can not understand how it will work ? Suppose, we have two implicit in separate classes with method intToFraction and fractionToDouble in FractionConversions object. But until and unless we use those method explicitly, compiler will behave in same manner. – Gunjan Shah Aug 23 '21 at 02:12
  • @GunjanShah Standard way to handle ambiguous implicits in easy cases is Low Priority pattern https://stackoverflow.com/questions/33544212 In hard cases there are `shapeless.LowPriority`, `shapeless.Refute`/`implicitbox.Not`, `shapeless.OrElse`/`implicitbox.Priority` https://stackoverflow.com/questions/57933865 https://stackoverflow.com/questions/60232605 https://stackoverflow.com/questions/58027241 https://stackoverflow.com/questions/68046952 – Dmytro Mitin Aug 23 '21 at 08:04
  • @GunjanShah By the way, `shapeless.OrElse`/`implicitbox.Priority` itself is an example of Low Priority pattern https://github.com/milessabin/shapeless/blob/main/core/src/main/scala/shapeless/orelse.scala https://github.com/monix/implicitbox/blob/master/shared/src/main/scala/implicitbox/Priority.scala And `shapeless.Refute`/`implicitbox.Not` is implemented via ambiguous implicits in Scala 2 https://github.com/milessabin/shapeless/blob/main/core/src/main/scala/shapeless/refute.scala https://github.com/monix/implicitbox/blob/master/shared/src/main/scala-2/implicitbox/Not.scala – Dmytro Mitin Aug 23 '21 at 08:31
  • 1
    @GunjanShah this is what I mean with using extension methods instead: https://scastie.scala-lang.org/BalmungSan/OUHrKHIzR8CVdaR69F7vQw - You may also provide an overload of `Fraction.*` to accept doubles and another extension method `*` in the `IntOps` class to get the same API you wanted. – Luis Miguel Mejía Suárez Aug 23 '21 at 15:30

1 Answers1

2

Your characterization is correct: for an expression a.f(b) (note that a*b is just operator notation for a.*(b)), an implicit conversion applicable to b which allows the expression to type check will take precedence over an implicit conversion applicable to a. This arises because an implicit conversion (called a "view" in the language standard) of a could only be attempted if * (in Int) was not applicable to Fraction (see SLS 7.3)... however the definition of applicability in SLS 6.6 says that an implicit view is sufficient to be applicable.

Accordingly, if one wanted to implicitly convert 2 to a Fraction to use Fraction.*, one would need

(2: Fraction) * Fraction(1, 2)

Likewise, for case 3, if you want the fraction implicitly converted to a Double first:

(Fraction(4,5): Double) * 3
Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30