Saturday, November 7, 2009

Programming in Scala: Chapter 21 - Implicit Conversions and Parameters

Some notes from chapter-21, Programming in Scala.

Implicits are a way of "adding new methods" to a class. With implicits, you can make one object behave like other.

They are defined like regular methods except that they start with a keyword "implicit".

On finding something like x op y, compiler checks to see if op is defined on x or if there is an implicit converter available such that object returned from convert(x) has op and it replaces x with convert(x).

Rules - Here are 4 rules that govern implicit conversions.

1.Marking Rule: Only definitions marked implicit are available.
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.

"Single Identifier" means compiler will not try to use someVariable.convert, it has to be available as a single identifier. One usual practice is to define all the implicits in an object Preamble and client code can simply do "import Preamble._" to make all of them available.
However there is one exception to this rule, the compiler will also look for implicit definitions in the companion object of source or target type.For example if you pass a Dollar to a method that expects Euro, compiler will look inside companion objects of Dollar and Euro to see if there is any implicit available to convert Dollar to Euro.

2.Non-Ambiguity rule:
If there are two or more implicits available to do same conversion, compiler will raise an error.

3.One-at-a-time Rule: The compiler will never try to rewrite x + y into convert2(convert1(x))

4.Explicit-First rule: x + y will never be re-written if + is already defined for x.

Note: Usually name of the implicit does not matter.

Where are implicits tried?

They are tried in three places.

1.conversion to an expected type: For example you have a String and you might want to pass it to a function that expects RandomAccessSeq[Char].

2.conversion of the receiver of a selection: If a method is called on an object that doesn't have it for example x + y, if x of a type that doesn't define + then implicits will be tried. One very interesting example of this is in supporting the syntax like.. Map(1 -> "one", 2 -> "two", 3 -> "three"), -> is a method in a class named ArrowAssoc and scala.Predef contains an implicit to do the conversion. Its defined as follow...
package scala
object Predef {
class ArrowAssoc[A](x: A) {
def -> [B](y: B): Tuple2[A, B] = Tuple2(x, y)
implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] =
new ArrowAssoc(x)

This kind of "rich wrappers" pattern is pretty common in libraries that provide syntax-like extensions to the language.

3.implicit parameters:
You can define method like
def meth(a: A)(implicit b:B, c:C)

You can call meth like a regular method by providing all the parameter list, or you can optionally leave out the whole implicit parameter list as following.

In this case, compiler will look for implicit vals defined of type B,C to insert automatically. So one should make them available like..

implicit val bVal = new B..
implicit val cVal = new C..

As a style rule, it is best to use a custom named type in the types of implicit parameters. For example it is not advised to have following..
def meth(a: A)(implicit b: String)
because String is a very common type and there might be implicit values available of type String that you don't know about so you're better off wrapping the String in a custom type.

View bounds:
Look at the following method...
def maxListImpParm[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 = maxListImpParm(rest)(orderer)
if (orderer(x) > maxRest) x
else maxRest

Since orderer is implicit, it can be left out in the method body at both the places and following code means the same..
def maxListImpParm[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 = maxListImpParm(rest) //(orderer) is implicit
if (x > maxRest) x //orderer(x) is implicit
else maxRest

Notice, in the second definition, there is no mention of orderer and hence the name does not matter. Since this kind of pattern is very common in scala, scala lets you leave out the name of this parameter and shorten the method header by using a view bound.. above method with new signature will be written as..

def maxListImpParm[T <% Ordered[T]](elements: List[T])

Here [T <% Ordered[T]], means "Any T can be used as long as there is an implicit available to convert it to Ordered[T]". By default an identity implicit converter is always available that would convert Ordered[T] to Ordered[T].

Debugging the Implicits:
Sometimes you might wonder why the compiler did not find an implicit conversion that you think should apply. In that case it helps to write the conversion out explicitly. If that also gives an error message, you then know why the compiler could not apply your implicit.

If above works, then you know one of the other rules(such as scope rule) is preventing the use of converter by the compiler.

When you are debugging a program, it can sometimes help to see what implicit conversions the compiler is inserting. The Xprint: typer option to the compiler/interpreter is useful for this. If you run scalac with this option, then the compiler will show you what your code looks like after all implicit conversions have been added by the type checker.

As a word of warning, implicits can make code confusing if they are used too frequently. Thus, before adding a new implicit conversion, first ask whether you can achieve a similar effect through other means, such as inheritance, mixin composition, or method overloading. If all of these fail, however, and you feel like a lot of your code is still tedious and redundant, then implicits might just be able to help you out.

No comments:

Post a Comment