Implicit conversion is a common operation in Scala’s advanced applications. It is essentially based on the OCP open close principle, and it can be used in a variety of ways to implement decorator patterns, or to implement mapping capabilities, or to choose to hide parts of the code that are cumbersome, obscure, and don’t require deep understanding for the caller.

In addition, since Scala supports the use of symbols as function identifiers, the combination of implicit transformations allows for quite a few internal syntactic features (you can also call it an internal DSL). In particular, for collections and element operations, Scala replaces long English function names such as concat, add, and remove with a lot of shorthand notation, and we can just write collections as if we were writing simple arithmetic. For example, for Scala’s Map collection, we can use the key -> value notation to graphically indicate that a key-value pair is stored. However -> is not a syntax or notation supported by Scala itself, but rather a “syntactic sugar” wrapped in implicit conversions. Why do you do that? In most cases, function identifiers using simple symbols as mnemonics are much more intuitive than a long list of English words. This is perfect for Scala’s pursuit of simplicity and sophistication. For example, we tend to use 1 + 1 for “1 plus 1” rather than 1 add 1 or 1.add(1).

In this chapter, I introduce the concepts of implicit conversions and custom operators, and demonstrate the power of implicit conversions through an example of self-implementing “unit conversions”. Implicit conversions provide flexibility to Scala, but there is also an indescribable “mystery” that makes it difficult to understand the magic code of some Scala libraries directly, because there are so many implicit conversions inside the libraries.

Implicit conversion function

Let’s start with a basic example. All Scala data types provide a toXXX method for explicitly converting data:

val double2Int: Int = 3.5.toInt
println(s"double2Int = ${double2Int}")
Copy the code

However, when there is a need for this kind of data transformation in many parts of a piece of code, we have to attach toXXX methods everywhere. This is one of the main reasons implicit conversion is introduced: the compiler automatically identifies code that needs implicit conversion at compile time, and the program developer needs to provide the corresponding conversion methods: implicit conversion functions, implicit classes.

The IDEA compiler underscores code that has been implicitly transformed.

Implicit conversion functions convert data types

Continuing with the previous example, we want the conversion from Double toInt to be handled by the compiler itself (that is, implicitly), rather than having to do it manually through.toint every time. There’s a whole new key called implicit:

implicit def putInt(in : Double) :Int= in.toInt
Copy the code

The function name can be defined to reflect the function, because the compiler only relies on the function signature to search for the appropriate implicit function. The signature refers to the parameter list and return value of the function. Implicit conversion functions are typically Function

types that pass in parameters of type T that need to be consumed (or converted) and produce (or supply) converted values of type R. The signature difference here means that T and R are different in any way.
,r>

It is important to note that an implicit conversion function takes effect automatically in the current scope and subfields in which it is declared. Therefore, when declaring an implicit conversion function, it is especially important to pay attention to whether the upward scope defines an implicit conversion function with a duplicate signature.

If the implicit conversion function is declared inside the main function, all assignments from Double to Int inside the main function are automatically replaced by the compiler via the putInt function.

  def main(args: Array[String) :Unit = {

    implicit def putInt(in : Double) :Int= in.toInt
	
    // There is no error because an implicit conversion occurred.
    val int: Int = 3.5
    println(s"double2Int = $int")}Copy the code

Note that in development, we should define implicit conversion functions in the smallest possible scope to avoid the impact of implicit conversion functions on other scopes.

Observe implicit conversion functions from the decompilation file

Define two putInt implicit conversion functions with the same name in different scopes (one in the class, one in the function), compile the.scala file, and then use the JD-GUI decompiler to see how the two methods are defined:

package scalaTest

object ImplicitTransform {

  def main(args: Array[String) :Unit = {
    // Declare a function inside the function.
    implicit def putInt(in : Double) :Int= in.toInt

    val int: Int = 3.5
    println(s"double2Int = $int")}// Declare a function in the class.
  implicit def putInt(in:Float) : Int = in.toInt
}

Copy the code

Part of the ImplicitTransform$.class file is posted below:

public final class ImplicitTransform$
{
  / /... Ignore the MODULE$section
  public void main(String[] args)
  {
    int int = putInt$1(3.5 D);
    / /... Print data
  }
  public int putInt(float in) {
    return (int)in;
  }

  private final int putInt$1(double in)
  {
    return (int)in; }}Copy the code

The implicit conversion function putInt inside the main function was moved outside the main method at compile time and declared to be putInt$1 (to avoid collisions with the putInt method inside the class), protected with the final keyword. The compiler looks inside the main method for code that needs to be cast and replaces it with the putInt$1 function.

Map mapping is implemented using implicit transformations

Implicit conversions are typical Function

functions that can be used to implement map mapping. An example would be the Student class with age and name attributes. Now you want the compiler to gracefully concatenate the Student’s information into a string and return it to studentInfo instead of reporting an error.
,r>

implicit def printStudent(student: Student) = student.name + "-" + student.age

// Accept a Student object as a string. The compiler calls the printStudent implicit conversion function to handle this automatically.
val studentInfo: String = new Student(age = 23, name = "Wang Fang")
println(s"studentInfo = ${studentInfo}")
Copy the code

The console displays: studentInfo = Wang Fang-23.

OCP philosophy in implicit conversion functions

The OCP principle, the open and closed principle, is closed to modification and open to expansion.

Just to illustrate, let’s say we have JDBC native components, and we want to extend JDBC functionality without changing the original core code. In Scala, such requirements can be implemented using implicit functions. Given an immutable simulated JDBC class:

final class JDBC{
  val url : String = "127.0.0.1"
}
Copy the code

We “plugged-in” the new functionality to a more powerful class called Mybatis. It not only has the original content of JDBC, but also adds its own properties and methods:

class MyBatis(val url : String){

  val maxConnection : Int = 10

  def Pool() :Unit={
      print(S "To obtain connection pool information:$url")}}Copy the code

Declare an implicit conversion function to upgrade JDBC to Mybatis in the main function:

implicit def JDBC_(jDBC: JDBC) :MyBatis =new MyBatis(jDBC.url)
Copy the code

All JDBC instances defined in the main function domain can then use the new functionality and properties brought by Mybatis.

jdbc.Pool()
println(jdbc.maxConnection)
Copy the code

It looks as if JDBC directly acquires the functionality of Mybatis, but in fact the compiler “switches” JDBC into Mybatis components through implicit transformations.

JDBC_(jdbc).Pool(a)Copy the code

In this case, it’s easy to understand: another of Scala’s old tricks of graft (or, to give it a more elegant name, decorator mode).

Recall that dynamic mixing can serve a similar purpose:

trait enhancedJDBC{

  this:JDBC= >def Pool() : Unit ={
    print(S "To obtain connection pool information:$url")}val maxConnection : Int = 10
}
Copy the code

For JDBC instances that require extended functionality, you simply need to mix this feature in dynamically. The author’s understanding is that the characteristic is more inclined to dynamic plug and pull flexible assembly, while implicit function transformation is more inclined to hide the complicated transformation details. Note, however, that overuse of implicit conversions can greatly reduce the readability of your code.

Note: We can also use implicit classes for this example.

Implicit conversion functions summarize the main points

  1. The name of an implicit function does not affect the position of the compiler, which only relies on the function signature to match the appropriate implicit conversion function.
  2. The signatures of implicit functions should be distinguished without ambiguity.
  3. Implicit functions can be used to implement mapping or decorator functionality.
  4. Implicit functions cannot recursively call themselves.

Implicit class

Scala version 2.10 also allows classes to be declared with the implicit keyword, which is called an implicit class. Implicit classes are not much different from implicit functions in terms of usage and design philosophy, except that they encapsulate a set of enhancements and properties for one data type into another structure (OOP thinking).

Characteristics of implicit classes

  1. The constructor argument list has one and only one parameter, the type of which determines the target type to be enhanced for this implicit class.

  2. Implicit classes cannot be top-level objects; they always appear as inner classes or local members.

  3. Implicit classes cannot be template classes (template classes are related to pattern matching).

  4. A scope cannot have methods with the same name, inner classes with the same name.

  5. It can do what implicit conversion functions can also do.

Look at implicit classes from the perspective of underlying compilation

Let’s re-implement the JDBC upgrade to MyBatis example using implicit classes.

An implicit class must have one and only one argument, the type of which is the class to be extended.
implicit class Mybatis(jDBC: JDBC){
	
  val url: String = jDBC.url
  val maxConnection : Int = 10

  // The extension function can be defined internally.
  def Pool() :Unit ={
    println("Get connection pool")}def delete() :Unit ={
    println("Delete data")}}//------------ main function calling part -----------------//
val jdbc = new JDBC
jdbc.Pool()
println(jdbc.maxConnection)
Copy the code

As mentioned, we can’t make a top-level declaration of an implicit class in a. Scala file (i.e. declare it as a separate class). Because at the bottom, an implicit class is always compiled as if it were an inner class.

package scalaTest;
/ /...
public class ImplicitTransform$Mybatis$2
{
  private final String url;
  private final int maxConnection;

  // Only get methods are compiled for val variables.
  public String url(a){return this.url; } 
  public int maxConnection(a){ return this.maxConnection; }

  // New method of Mybatis extension
  public void Pool(a){/ /...
  }
  public void delete(a) {/ /...
  }
  
  // constructor
  public ImplicitTransform$Mybatis$2(JDBC jDBC)
  {
    this.url = jDBC.url();
    this.maxConnection = 10; }}Copy the code

We don’t need to worry about the large number of $signs in the.class file, just know that the compiler declares a JDBC -> Mybatis implicit conversion function in the scope where the implicit class takes effect. When a JDBC object calls a Mybatis member, the implicit conversion function is automatically used to replace JDBC with the Mybatis instance.

package scalaTest;
// Ignore part of the import
public final class ImplicitTransform$
{
  // Ignore MODULE$code
  public void main(String[] args)
  {
    JDBC jdbc = new JDBC();
    Mybatis$1(jdbc).Pool();
   	// Ignore other code
  }
  An implicit conversion function is declared where implicit classes are used.
  private final ImplicitTransform.Mybatis$2 Mybatis$1(JDBC jDBC)
  {return new ImplicitTransform.Mybatis$2(jDBC);}
}
Copy the code

Overview: Implicit conversion functions and implicit classes

Timing of implicit conversion

  1. When the reference type and the object to which it actually refers are not of the same class and cannot be converted to an upcast object.

  2. A class uses a property or method that does not exist on its own.

  3. View demarcation or context demarcation is used (this has to do with generics, which I’ll cover in my last few articles in Scala, because it has some more complex concepts than Java).

How implicit conversions work

If an assignment of the form S = T is implicitly converted:

  1. The compiler first looks for implicit classes, implicit functions that are available in context.

  2. If it is not found in the context, we drill down inside the T type to find an implicit conversion rule that is available, and the situation becomes more complicated:

    • If the typeTMixed with the idiosyncrasies, it’s implicit parsingTThe compiler searches for these attributes as well.
    • ifTContains type parameters such asList[String], then the implicit conversionListStringWill be searched by the compiler.
    • ifTIs a path-dependent typeinstance.T, the compiler searches for objectsinstanceAnd the inner classT
    • ifTIs an inner class that uses a type projectionClazz#T, the compiler searchesClazzClass and inner classT.

In general, you should try to get the compiler to find the right conversion rule the first way. Otherwise, it not only increases the compiler’s workload, but also makes it difficult for subsequent code maintainers to pinpoint where implicit transformations are declared.

Implicit values and implicit parameters

Define implicit values

Implicit values are used to customize the default assignment of a data type and in conjunction with implicit arguments. Defining an implicit value requires the implicit keyword in front of it.

  In most cases, implicit values are not allowed to be tampered with, so we use val instead of var.
  implicit val defaultString: String = "null string."
  implicit val defaultInt : Int = 200
  implicit val defaultDouble : Double = 200.00d
Copy the code

Int (string, int); implicit (int, string, int); implicit (int, int); implicit (int, string, int); implicit (int, string, int); implicit (int, int);

def usingImplicitValue(implicit string: String,int: Int) :Unit = {
  println(string)
}
Copy the code

Implicit parameters mean that when the function is called without explicitly passing in parameters, its value is provided by an implicit value defined in the context. Implicit variables can also declare default parameter values, like this:

def usingImplicitValue(implicit string: String = "null String" ,int: Int = - 1) :Unit = {
  println(string)
}
Copy the code

This way, the default parameter values are used when the compiler does not find an implicit conversion available in the context.

Distinguish between different concepts

Not to be confused with implicit values and default values inside class declarations.

class Clazz{
  // _ placeholders to assign default values.
  val value : Int= _}Copy the code

Default values and implicit values do not serve the same purpose:

  1. Default values are used when initializing values, which are given by Scala: for exampleIntThe default value of0, the default value of the reference type defaults tonull
  2. Implicit values are used forThe developer specifies a default value for a data type in a contextIn other words, developers can specify implicit valuesIntThe default value of- 1Rather than0

Again, implicit values differ from default parameter values. For example, int has default values only:

// This is the default parameter value.
def function(int : Int = 100) :Int ={int * 2}
Copy the code

The difference between this and an implicit value is:

  1. Default parameter values take effect only if this function is called and no explicit assignment is made to the specified parameter.
  2. An implicit value can be used in any function input parameter in a scope that has an implicit parameter declared and whose type matches.

Use implicit values and implicit parameter details

The details of using implicit values and parameters are more verbose:

Similar to implicit conversion functions, only one implicit value of a data type is allowed within the same field and its subfields. When ambigouous implicit Values is ambigouous when the program is compiled, it indicates that there are multiple implicit values of the same data type. Therefore, when declaring an implicit value in a small scope, it is important to note whether an implicit value of the same type already exists in the large scope up.

A list of implicit parameters that can be omitted when calling a function, indicating that all of its implicit parameters use implicit values provided by the context.

// If all arguments are automatically assigned using implicit arguments, no parentheses.
usingImplicitValue
Copy the code

However, if you want the value of an implicit parameter to be provided by the default parameter value, you need parentheses () if the implicit parameter has a default parameter value.

//-------------- modify function ----------------//
def usingImplicitValue(implicit string: String = "default value") :Unit = {
    println(string)
}
//---------------- main function ----------------//
// Implicit values are used for all parameters in the parameter list, regardless of whether the default values exist.
usingImplicitValue

// This method calls the default argument values in the line.
usingImplicitValue()
Copy the code

In addition, the author strongly suggests that if only some parameters in the parameter list have default values, the assignment should clearly indicate which value value is assigned to which variable name by writing name = value.

def usingImplicitValue(explicitInt : Int = 100, explicitDouble : Double) (implicit double : Double,  int: Int =12) :Unit = {
      println(double)
      println(int)
      println(explicitInt)
}
// Specify the assigned parameters and values using the name = value format.
usingImplicitValue(explicitDouble = 23)(double = 12)
Copy the code

As shown in the code block above, if a function has both ordinary and implicit arguments, it should be represented by a separate argument list, and the argument list for the implicit argument is always in the last position. You cannot have both implicit and non-implicit parameters in the same parameter list. If the keyword implicit appears at the beginning of a parameter list, it indicates that all parameters in the list are implicit.

Such a function is called with multiple argument lists represented by parentheses () assigned separately.

def usingImplicitValue(explicitInt : Int = 100) (implicit double : Double =10.00,  int: Int=100) :Unit = {
    // All arguments in the first argument list are non-implicit.
	println(explicitInt)
    
    // All arguments in the second argument list are implicit.
	println(double)
	println(int)    
}
/ / -- -- -- -- -- -- -- -- -- -- -- -- -- said all adopt the explicit assignment -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / /
usingImplicitValue(101) (21.23)
/ / -- -- -- -- -- -- -- -- -- -- -- -- -- said all adopt the default parameter value -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / /
usingImplicitValue()()
//-- indicates that the previous parameter list uses the default parameter value, and the implicit parameter uses the implicit value --//
usingImplicitValue()
/ / -- -- -- -- -- -- -- -- -- -- -- -- -- of some parameters to specify the assignment -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / /
usingImplicitValue(explicitInt = 101)(int =201)
Copy the code

Summary of the three

  1. When a parameter list is internal toimplicitKeyword: indicates that the list is filled with implicit parameters. It is not needed when calling a function(a)Assign separate values to argument lists that contain implicit arguments, unless you want to overwrite them explicitly. The context must declare an implicit value for each implicit parameter, or an implicit parameter must have a parameter default value. Otherwise, an error will appear:could not find implicit value for parameter
  2. The compiler looks for matching implicit values in the context first before attempting to find default parameter values. If an implicit parameter has neither an implicit value nor an inline default value, neither does the calling function(a)When an active parameter is passed, the compiler reports an error. However,Mixing implicit and default parameter values is not recommended because such code can be very confusing.
  3. Others are not usedimplicitA parameter list that begins with a keyword (that is, a parameter list in the general sense) is either actively assigned at call time or has a default parameter value within the line. If neither exists, an error is reported:not enough arguments for method xxx

Custom operators

Scala allows you to use symbols other than letters as function identifiers. We can choose to override the +,-,*,\ operators, or reproduce the “increment” and “decrement” functions commonly used in other languages such as ++, –, or implement simple internal DSL features. The basic implementation of a DSL will be further elaborated in a subsequent Scala chapter through parser combinators, which will be a combination of implicit transformation and functional programming.

Custom middle operator

For functions with a single argument list, you can think of the function’s identifier as a binocular (or central) operator: the first argument is the object that called the function itself (this), and the second argument is the unique argument in the argument list.

For example, here is a Wallet class that overloads the + symbol, and the function takes another Wallet instance as an argument. Add the balance of the current Wallet object to another Wallet object and return a new Wallet object.

object OperatorOverride {

  def main(args: Array[String) :Unit = {

    val wallet1 = Wallet(20)
    val wallet2 = Wallet(30)

    // Call the custom + method
    println((wallet1 + wallet2).balance)
  }
}

// This is a sample class that provides the apply method by default.
case class Wallet(var balance:Int = 0){
  // Define the addition of a wallet class: return a new wallet with the sum of the balances of the two wallets.
  def +(wallet: Wallet) :Wallet= {Wallet(this.balance + wallet.balance)
  }
}
Copy the code

Previously, we usually needed to implement the call in this way:

val wallet3 : Wallet = wallet1.+(wallet2)
Copy the code

Scala, however, leaves an interesting syntactic sugar behind. We can replace. Accessors with infix expressions:

// Is actually equivalent to wallet1.+(wallet2)
val wallet3 : Wallet = wallet1 + wallet2
Copy the code

This is also true for the following postpended operators and the preceding operators.

Custom postset operator

If we define a function that has no arguments, we can think of the function’s identifier as a post-operand operator that does not require another operand (a monocular operator), typically the ++ we use in other languages, or –, etc.

// Add method to Wallet:
  // Define wallet increment method: wallet balance +1, and return the balance.
  def ++() : Int = {
    // The ++ operator cannot be nested inside a function.
    this.balance +=1
    balance
  }
//----------- main function call ------------//
	print(wallet1 ++)
	// When used alone, add; Indicates the end of the statement.
	wallet ++;
Copy the code

However, the compiler may interpret wallet ++ as an infix expression that has not been written, and raise an error by treating the next line as an input parameter (because the ++ method requires no additional arguments). To avoid this misconception, it is best to add; at the end of the line after the postposition operator. Finish.

Custom pre-operator

In the same way, we can also rewrite some typical leading operators, such as taking inverse operators! . We can redefine the leading operator in a class! What it actually means: like a statement! Wallet stands for emptying the wallet’s balance. Note that when declaring the pre-operator, the function identifier is preceded by an additional prefix unary_.

Other pre-operators that can be used are: +, -, ~. Unlike the middle and postposition operators, any symbol/English identifier other than these four symbols cannot be used with the pre-position operator.

// Add method to Wallet:
   // Define a pre-operator! , and you need to mark it with unary_.
  def unary_!() : Int= {this.balance = 0
    balance
  }
//--------- main function call -----------//
 // Call custom! Methods.print(! wallet1)Copy the code

Example: Implement implicit unit conversion

For most tools, time parameters are set in milliseconds. For example, to sleep the current thread for 3 seconds, we need to convert to 3000 milliseconds as a parameter:

Thread.sleep(3000)
Copy the code

Now try implementing syntactic sugar like this: replace 3000 with 3 second to make the program more readable.

The numeric part of the time is represented by an Int, so we can create an implicit class (or function) that accepts an Int for the value of the time. When we call the second, minute, and other postpositions (we call them “units” conventionally), Let the program automatically convert it to the corresponding millisecond value in units. The code implementation is shown below:

implicit class TimeDuration(millis_ : Int) {

    def millis : Int = millis_
    def second : Int = millis_ * 1000
    def minute : Int = millis_ * 60 * 1000

} 
Copy the code

Now if we want to express 3 seconds, we can simply substitute 3 seconds. To express a minute, you just have to express it in terms of 1 minute, not 60 times 1000. We only need to convert the zipgex ^ (^) _ itself without any other variables. Therefore, all the millis, seconds and other functions are parameterless without parentheses.

// The number + unit notation is more logical.
Thread.sleep(3 second)
Copy the code

Why I sometimes choose a no-argument function and why I sometimes choose an empty parenthesis function depends on whether the function itself has side effects — I’ll explain that in a later section on Scala functions.