Implicit conversion is the scala ways to allow you to extend libraries code or in another word, code written by others. Similar construct in other langauges include modules in Ruby, packages to add more classes to each other's classes. C# has extended method which allow more restrict code added.
In this chapter, we will inttroduce topics such as follow.
- Implicit conversions
- Rules for implicit
- Implicit conversion to an expected type
- Converting the receiver
- Implicit parameters.
- View Bounds
- Debugging implicit
Implicit conversions
before dive into the implicit conversions, we will show some example to tell why the implicit conversion is useful. in a short word, the implicit conversion is useful when we need to work with two bodis of librarie that was developed without each other in mind, they may have their own way to encode the a concept which is essentially the same thing.
e.g of the Swing libraries, an example is follow.
// this is Java
val button = new JButton
button.addActionListener(
new ActionListener {
def actionPerformed(event : ActionEvent) {
println("pressed!")
}
}
)
and we canot do the following.
// we cannot do that because the addActionListener do not accept a function type
button.addActionListener(// type Mismatch )
(_: ActionEvent) => println("pressed!")
)
and now we define the implicit conversion. wihch can convert a lambda to a ActionListner anonymous instance. let's see the implicit function definition.
// -- implicit conversion
import java.awt._
import javax.swing._
import java.awt.event._
implicit def function2ActionListener(f : ActionEvent => Unit) =
new ActionListener {
def actionPerformed(event : ActionEvent) = f(event)
}
you can call the conversion explicittly.
// you can do with the explicit call to function2ActionListener
button.addActionListener(// type Mismatch )
function2ActionListener(
(_: ActionEvent) => println("pressed!")
)
)
or you can apply it impliictly, just by make it visible in the scope and then you can do
// now, the essence of the implicit conversion is that you can do with this now
button.addActionListener(
(_: ActionEvent) => println("pressed!")
)
Rule for implicit
- rules 1: Marking rule: only definitions marked implicit are available
- rules 2: Scope Rule : An inserted implicit conversion must be in scope as a single identifier, or be associated with the source or target type of the conversion
- rules 3: one-at-a-time rule: only one implicit is tried
- rules 4: Whenever code type checks as it is written, no implicit are attempted.
// for rule 2
object Dollar {
implicit def dollarToDollar(x : Dollar) : Eruo = // ...
}
class Dollar { ... }
and for rule 3.
this means that the compiler never try something
convert1(Convert2(x)) + y
for
x + y
the rules 4 state that for any code that works, implicit will not change it.
Implicit conversion to an expected type
a simple example is
val i : Int = 3.5 // error , convert from a double to int which is lower in precision
implicit def doubleToInt(x : Double) = x.toInt
val i :Int = 3.5 // works, becuase there is a implicit conversion doubleToInt in scope
// it is equivalent to the following
val i : Int = doubleToInt(3.5)
// Scala.Predef has this method , or somehting similar to the above
implicit def intToDouble(x :Int) = x.toDouble
though it is not a good practise to downgrade the precision of an operand, it shows that you can convert from one type to another this can be useful, especially when you need a specified type to operate.
Converting the receiver
Implicit conversion can apply to the receiver of the method call, the object on which the method is invoked. this offers two benefit, one is that it allow smoother integration of a new class into an existing class hierarchy. and second it help in the DSL language.
Integrating with new types
we have get exposure to the Rational class, the Rationalclass is as follow.
// -- Converting the receiver: interoperating with new types
class Rational(n : Int, d : Int) {
require(d != 0) // this is how you do on "Checking precondition"
private val g = gcd(n, d)
val number = n / g
val denom = d / g
// this will define the auxiliary constructor,
// the auxiliary constructor must call the primary constructor in its first action
def this(n : Int) = this(n, 1)
// below show that how you can do override in Scala
override def toString() : String = number + "/" + denom
private def gcd(a : Int, b : Int) : Int = {
if (b == 0) a else gcd(b, a % b)
}
def +(that: Rational) : Rational = {
new Rational(number * that.denom + that.number * denom, denom * that.denom)
}
def *(that : Rational): Rational = {
new Rational(number * that.number, denom * that.denom)
}
}
and we can do things like
val oneHalf = new Rational(1 , 2)
oneHalf + oneHalf
oneHalf + 1
but we cannot do this
1 + oneHalf
but if we define a implicit converson
// you cannot do 1 + oneHalf
implicit def intToRatioanl(x : Int) = new Rational(x, 1)
then it is possible.
Simulating new syntax
a native example that we have.
//-- Converting the receiver: simulating new syntax
Map(1 -> "one", 2 -> "two", 3 -> "three") // how is this possible?
how does this works? "->" can essentially works for any types, but how does this work?
// the reason is because of this:
package Scala
object Predef {
// define a new class, which has the method '->'
class ArrowAssoc[A](x : A) {
def -> [B](y : B) : Tuple2[A, B] = Tuple2(x, y)
}
// and a implicit conversion to convert x of any type to ArrowAssociate which is capable of returning a tuple
implicit def any2ArrowAssoc[A](x : A) : ArrowAssoc[A] = new ArrowAssoc(x)
}
Implicit Parameters
not only function that can be made implicit the parameter of a function can be also made implicit, what is the use of implicit parameters, let's see.
class PreferredPrompt (val preference : String)
object Greeter {
def greet(name : String)(implicit prompt : PreferredPrompt) {
println("welcome . " + name + ". the system is ready.")
println(prompt.preference)
}
}
val bobsPrompt = new PreferredPrompt("relax>")
Greeter.greet("Bob")(bobsPrompt)
however, this is not using the implicit parameter, object JoesPrefs defines a scope where implicit parameter can be defined.
// now you can define a module
object JoesPrefs {
implicit val prompt = new PreferredPrompt("yes, master> ")
}
but doing this still do not give you the ability to use it implicit. you have to first import the modules.
// now import the the implicit def and then you can call the implicit parameters
import JoesPrefs._
Greeter.greet("Joe")
one golden rule about the implicit parameter in that the implicit parameter keyword applies to an entire parameter list. the following examples.
// keyword applies to an entire parameters list.
class PreferredPrompt(val preference : String)
class PreferredDrink(val preference : String)
object Greeter {
def greet(name : String) (implicit prompt : PreferredPrompt, drink: PreferredDrink) { // implicit applies to both "prompt" and "drink"...
println("Welcom, " + name + ". The system is ready.")
println("but while you work,")
//print("but while you work,")
println("why not enjoy a cup of " + drink.preference + "?")
println(prompt.preference)
}
}
object JoesPrefs {
implicit val prompt = new PreferredPrompt("yes, master> ")
implicit val drink = new PreferredDrink("Tea ")
}
import JoesPrefs._
Greeter.greet("Joe")
in the code above, the two parameters "prompt" and "drink" are both implicit, as the implicit applies to the parameter lists.
general speaking, what implicit paramter does is to replace someCall(a) with some someCall(a)(b), or new SomeClass(a) with new SomeClass(a)(b), or thereby adding a missing parameter list to complete a function call if the missing paramter takes three parameters, the compiler might replace someCall(a) with someCall(a)(b, c, d);
a general rule of defining implicit parameter is that you will use rare or special enough types, as the "PreferredPrompt" and "PreferredDrink" types on the code above.
another thing to know about the implicit parametes is that they are perhaps most often used to provide information about a type mentioed explicit earliy in an earlier parameter lists.
first see the code the first version.
// first version of the maxListUpBound
def maxListUpBound[T <: Ordered[T]](elements : List[T]) : T =
elements match {
case List() => throw new IllegalArgumentException("empty list ")
case List(x) => x
case x :: rest => val maxRest = maxListUpBound(rest)
if ( x > maxRest) x
else maxRest
}
now, with an ordered code, as the following.
// a more general maxListUpBound will require a separate, second argument, in addition to the List[T] argument, a function that
// converts to a T to Ordered[T]
def maxListUpBound[T](elements : List[T])(implicit orderer : T => Ordered[T]) : T =
elements match {
case List() => throw new IllegalArgumentException("empty list ")
case List(x) => x
case x :: rest => val maxRest = maxListUpBound(rest)(orderer) // => added ordered
if ( orderer(x) > maxRest) x // ordered (x) will convert the T => Ordered[T]
else maxRest
}
as in the above code, the ordered parameter provides more information about type T - in this case, how to order Ts...
the standarded scala has implicit orderd methods for common types. so that you can run . so that you can run the code as follow.
maxListUpBound(List(1, 5, 10, 3))
maxListUpBound(List(1.5, 5.2, 10.7, 3.14159))
though we can re-writte the code as now. however, there are some style problems...
// some style problems .
// think of why ?
def maxListUpBound[T](elements : List[T])(implicit orderer : (T, T) => Boolean ) : T =
elements match {
case List() => throw new IllegalArgumentException("empty list ")
case List(x) => x
case x :: rest => val maxRest = maxListUpBound(rest)(orderer) // => added ordered
if ( orderer(x, maxRest)) x // ordered (x) will convert the T => Ordered[T]
else maxRest
}
can you see what it is? the problem with the above is that (T, T) => Boolean does return a comparere, however the problem is that many more that has (T, T) => Boolean , which means that the code is too generic to give any additional meaning.
View bound
Note on the code we can further simplify but we did not, the code.
case x :: rest => val maxRest = maxListUpBound(rest)(orderer) // => added ordered
if ( orderer(x, maxRest)) x // ordered (x) will convert the T => Ordered[T]
we are still using explicit maxListUpBound(rest)( orderer), or if ( orderer(x, maxRest)... know that not only the compiler try to supply that parameter as an available implicit in the body of the method,, thus both uses of the within the body of the method can be left out. so the code can be written as following?
def maxListUpBound[T](elements : List[T])(implicit orderer : T => Ordered[T]) : T =
elements match {
case List() => throw new IllegalArgumentException("empty list ")
case List(x) => x
case x :: rest => val maxRest = maxListUpBound(rest) // => ordered now is now one of theimplicit parameter , so you can left maxListUpBound(rest)(orderer)
if ( x > maxRest) x // now you don't need to write ordered(x), just x will do
else maxRest
}
so we can use any name for the implicit parameter, given that the name does not matter because it will be ignored by the the supplied parameter.
def maxList[T](elements: List[T])(implicit converter : T => Ordered[T]):T =
// .. smae body
def maxList[T](elements: List[T])(implicit iceScream : T => Ordered[T]):T =
// .. smae body
so the pattern is common, , Scala lets you leave out the name of the parameter and shorten the method header by using a view bound. using a view bound, you can write code as following.
// -- this is called View Bound
def maxListUpBound[T <% Ordered[T]](elements : List[T]) : T =
elements match {
case List() => throw new IllegalArgumentException("empty list ")
case List(x) => x
case x :: rest => val maxRest = maxListUpBound(rest) // (orderer) is implicit
if (x > maxRest) x // ordered (x) implicit
else maxRest
}
the syntax that import here is T <% Ordered[T], which can be explained in this way that I can use any T as long as the T can be treated as an Ordered[T]..
with this method, you can pass a List whose elements are themselves Ordered[T]... , that still can works, because the Scala has thsi implicit declared.
// implicit identity function
implicit def identity[A](x : A) : A = x
When multiple conversions apply
it can happens when multiple conversions can apply. for most partr, the Scala can choose to refuse to insert conversion in such a case.
// -- When multiple conversions apply
def printLength(seq : Seq[Int]) = println(seq.length)
// which one will that be picked ?
implicit def intToRange(i :Int) = 1 to i
implicit def intToDigits(i : Int) = i.toString.toList.map(_.toInt)
it can be too much ambiguity if you try to call printLength(seq : Sequence[Int]) = println(seq.length).
general rule of which implicit conversion is more specific than another if one of the following applied.
* the argument type of the former is subtype of the later's
* Both conversion are methods, and the enclosing class of the former extends the enclosing class of the latter.
e.g. "cba" is String, StringOps has many method such as reverse, but instead of returning a collection
it returns a String,
there is a implicit reverse methods, defined in LowPriorityImplicts,which has a conversion to a Sca;a collectoin, and the LowPriorityImplicit and which is extended by a Predef..
Debugging tips
now, comes some debugging tips.
object Mocha extends Application {
class PreferredDrink(val preference : String)
implicit val pref = new PreferredDrink("mocha")
def enjoy(name : String)(implicit drink : PreferredDrink) {
println("Welcom, " + name)
println(". enjoy a ")
//print("but while you work,")
println(drink.preference)
println("!")
}
enjoy("reader")
}
the code above, you can run with the following.
scalac -xprint:typer mocha.scala
you will see what implicit values has been supplied and that can help you finding out what is being provided to the value.
and another tips is that you can explicit supply the implicit conversion.
suppose that you take WrapString as the conversion from string to Lists instead of IndexedSeq, you will get some error, you can find it out by this:
// write the conversion explicitly
val chars :List[Char] = "xyz"
// :7: error: type mismatch;
// found : java.lang.String("xyz")
// required: List[Char]
val chars :List[Char] = wrapString("xyz")
//:7: error: type mismatch;
// found : scala.collection.immutable.WrappedString
// required: List[Char]
// val chars :List[Char] = wrapString("xyz")