Reputation: 1124
In Scala, I want to generate some aliases for basic types, and then implement conversions through a type class. This is both useful for me, and an opportunity to understand type classes. The code is the following:
type Index = Int
val Index = Int
type Integer = Int
val Integer = Int
type Real = Double
val Real = Double // to have companion object of Double also be the companion object of Real
trait Convertible[A] {
def toIndex(a: A): Index
def toInteger(a: A): Integer
def toReal(a: A): Real
}
implicit val ConvertibleIndex: Convertible[Index] = new Convertible[Index] {
def toIndex(i: Index) = i
def toInteger(i: Index) = i
def toReal(i: Index) = i.toDouble
}
implicit val ConvertibleInteger: Convertible[Integer] = new Convertible[Integer] {
def toIndex(i: Integer) = i
def toInteger(i: Integer) = i
def toReal(i: Integer) = i.toDouble
}
implicit val ConvertibleReal: Convertible[Real] = new Convertible[Real] {
def toIndex(r: Real) = r.toInt
def toInteger(r: Real) = r.toInt
def toReal(r: Real) = r
}
implicit val ConvertibleString: Convertible[String] = new Convertible[String] {
def toIndex(s: String) = s.toInt
def toInteger(s: String) = s.toInt
def toReal(s: String) = s.toDouble
}
implicit class ConvertibleSyntax[A](a: A)(implicit val c: Convertible[A]) {
def toIndex = c.toIndex(a)
def toInteger = c.toInteger(a)
def toReal = c.toReal(a)
}
Consider now:
val a = 3.toReal
val b = 3.0.toReal
val c = "3".toReal
The statement for a
does not compile, with the compilation error: method toReal is not a member of Int
. But, for the b
and c
statements, the implicit conversion to ConvertibleSyntax
is properly done.
Why is the implicit conversion not working on Int
, but is working on Double
and String
?
Upvotes: 1
Views: 206
Reputation: 8249
I think you might be a little confused about how Scala does implicit conversions. (A common mistake, as implicit
is a little overused.)
I think that what you want, first of all, is an implicit conversion function - or even an implicit class. Here's how you could do this using the latter:
Note: Int
, Index
and Integer
are treated identically, so are Real
and Double
, confusing matters somewhat, so I've pared this down to something that will work. Also, Convertible
does not need to be generic as its conversion functions need no arguments. Finally, you shouldn't have both type
and val
declarations for your types.
type Index = Int
type Integer = Int
type Real = Double
trait Convertible {
def toIndex: Index
def toInteger: Integer
def toReal: Real
}
// Implicit classes cannot be defined in top-level scope, so they belong to an object.
object Implicits {
implicit class ConvertibleInt(i: Int)
extends Convertible {
override def toIndex = i
override def toInteger = i
override def toReal = i.toDouble
}
implicit class ConvertibleDouble(d: Double)
extends Convertible {
override def toIndex = d.toInt
override def toInteger = d.toInt
override def toReal = d
}
implicit class ConvertibleString(s: String)
extends Convertible {
override def toIndex = s.toInt
override def toInteger = s.toInt
override def toReal = s.toDouble
}
}
Now try this:
import Implicits._
val a = 3.toReal
val b = 3.0.toReal
val c = "3".toReal
What's happening here? Well, the implicit class
declarations define classes that decorate the sole constructor argument with additional functions. If the compiler sees that you're trying to call a method on a type that doesn't have that method, it will look to see if there's an implicit conversion, in scope, to a type that does. If so, it is used and the function is called; if not, you get a compiler error. (The import
statement is used to bring the classes into your current scope.)
So, for example, when the compiler sees "3".toReal
it firstly determines that "3"
is a String
. Since this type doesn't have a .toReal
member, it tries to find a conversion from a String
to a type that does have such a member. It finds the ConvertibleString
implicit class that takes a String
argument and provides a .toReal
method. Yay! So the compiler creates an instance of this class by passing "3"
to ConvertibleString
's constructor, then calls .toReal
on the result.
On the other hand, when implicit
is used with a value, it tells the compiler that the value is a default for any matching implicit arguments of the same type that are not provided. NEVER USE implicit
WITH A PRIMITIVE OR COMMON LIBRARY TYPE!
For example:
final case class Example(i: Int)
// Default.
implicit val nameCanBeAnythingAtAll = Example(5)
// Function with implicit argument.
def someFunc(implicit x: Example): Unit = println(s"Value is $x")
Now, if you write something like this:
someFunc
the output will be Value is Example(5)
.
implicit
values and arguments are an advanced topic, and I wouldn't worry about how they're used right now.
Upvotes: 1
Reputation: 1770
Because you define ambiguous implicits for Index
and Integer
(both Int
).
Which one should be chosen by compiler?
Upvotes: 4