2016年9月13日星期二

Programming in Scala -- Chapter 21. Implicit Conversions and Parameters

1.  Implicit conversions are often helpful for working with two bodies of software that were developed without each other in mind. Each library has its own way to encode a concept that is essentially the same thing. Implicit conversions help by reducing the number of explicit conversions that are needed from one type to another.

2.  Implicit definitions are those that the compiler is allowed to insert into a program in order to fix any of its type errors. Implicit conversions are governed by the following general rules:
  • Marking rule: Only definitions marked implicit are available. You can use implicit to mark any variable, function, or object definition. Variables and singleton objects marked implicit can be used as implicit parameters.
  • 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. If you want to make someVariable.convert available as an implicit, you would need to import it, which would make it available as a single identifier. The compiler will also look for implicit definitions in the companion object of the source or expected target types of the conversion. For example, if you're attempting to pass a Dollar object to a method that takes a Euro, the source type is Dollar and the target type is Euro. You could, therefore, package an implicit conversion from Dollar to Euro in the companion object of either class, Dollar or Euro.
  • One-at-a-time rule: Only one implicit is inserted. The compiler will never rewrite x + y to convert1(convert2(x)) + y. However, it's possible to circumvent this restriction by having implicits take implicit parameters.
  • Explicits-first rule: Whenever code type checks as it is written, no implicits are attempted. You can always replace implicit identifiers by explicit ones, thus making the code longer but with less apparent ambiguity.

3.  There are three places implicits are used in the language: conversions to an expected type, conversions of the receiver of a method, and implicit parameters.

4.  Implicit conversions to an expected type let you use one type in a context where a different type is expected. The rule is simple. Whenever the compiler sees an X, but needs a Y, it will look for an implicit function that converts X to Y.

5.  The scala.Predef object, which is implicitly imported into every Scala program, defines implicit conversions that convert "smaller" numeric types to "larger" ones.

6.  Implicit conversions also apply to the receiver of a method call, the object on which the method is invoked. This kind of implicit conversion has two main uses. First, receiver conversions allow smoother integration of a new class into an existing class hierarchy. And second, they support writing domain-specific languages (DSLs) within the language.

7.  -> is a method of the class ArrowAssoc, a class defined inside the standard Scala preamble (scala.Predef). The preamble also defines an implicit conversion from Any to ArrowAssoc:

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)

    ...

}

8.  An implicit class is a class that is preceded by the implicit keyword. For any such class, the compiler generates an implicit conversion from the class's constructor parameter to the class itself. An implicit class cannot be a case class, and its constructor must have exactly one parameter. Also, an implicit class must be located within some other object, class, or trait.

9.  The compiler will sometimes replace someCall(a) with someCall(a)(b), or new SomeClass(a) with new SomeClass(a)(b), thereby adding a missing parameter list to complete a function call. It is the entire last curried parameter list that's supplied, not just the last parameter. Every argument you provide as the default values of the parameters should be marked implicit and the whole last parameter list should be marked as implicit. Note that the implicit keyword applies to an entire parameter list. The compiler selects implicit parameters by matching types of parameters against types of values in scope, implicit parameters usually have "rare" or "special" enough types that accidental matches are unlikely.

10.  The maxListOrdering function below is an example of an implicit parameter used to provide more information about a type mentioned explicitly in an earlier parameter list. To be specific, the implicit parameter ordering, of type Ordering[T], provides more information about type T—in this case, how to order Ts. Type T is mentioned in List[T], the type of parameter elements, which appears in the earlier parameter list. Because elements must always be provided explicitly in any invocation of maxListOrdering, the compiler will know T at compile time and can therefore determine whether an implicit definition of type Ordering[T] is available. If so, it can pass in the second parameter list, ordering, implicitly:

def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T

This pattern is so common that the standard Scala library provides implicit "ordering" methods for many common types.

11.  When you use implicit on a parameter, not only will the compiler try to supply that parameter with an implicit value, but the compiler will also use that parameter as an available implicit in the body of the method.

12.  The following method is defined in the standard library:

def implicitly[T](implicit t: T) = t

The effect of calling implicitly[Foo] is that the compiler will look for an implicit definition of type Foo. It will then call the implicitly method with that object, which in turn returns the object right back. Thus you can write implicitly[Foo] whenever you want to find an implicit object of type Foo in the current scope.

13.   Instead of below function definition:

def maxList[T](elements: List[T])(implicit ordering: Ordering[T]) : T

Scala lets you leave out the name of this parameter and shorten the method header by using a context bound. Using a context bound, you would write the signature of maxList as below :

def maxList[T : Ordering](elements: List[T]): T

The syntax [T : Ordering] is a context bound, and it does two things. First, it introduces a type parameter T as normal. Second, it adds an implicit parameter of type Ordering[T]. When using a context bound you don't know what the implicit parameter will be called. It's usually used to provide an implicit parameter for some function call within maxList.  A context bound is quite flexible. It allows you to use code that requires orderings—or any other property of a type—without having to change the definition of that type.

14.  If multiple implicit conversions are in scope and each would work, Scala refuses to insert a conversion in such a case. If one of the available conversions is strictly more specific than the others, then the compiler will choose the more specific one. To be more precise, one implicit conversion is more specific than another if one of the following applies:
  • The argument type of the former is a subtype of the latter's.
  • Both conversions are methods, and the enclosing class of the former extends the enclosing class of the latter.
Method overloading has the same relaxation. If one of the available foo methods takes a String while the other takes an Any, then the String version is chosen for foo(null) call.

15.  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 is useful for this. If you run scalac with this option, the compiler will show you what your code looks like after all implicit conversions have been added by the type checker.

没有评论:

发表评论