Use the infix function to build a more readable syntax

In the previous Kotlin study notes, we’ve used syntactic constructs like A to B many times to build key-value pairs, including Kotlin’s built-in mapOf() function.

The advantage of this syntax structure is that it is readable, and it is much closer to writing a program using English syntax than calling a function. You may be wondering, how does this work? Is “to” a key word in Kotlin? In this section, we will decrypt this function in depth.

First of all, to is not A keyword in Kotlin, and we can use syntactic constructs like A to B because Kotlin provides A high-level syntactic sugar feature: the infix function. Of course, the infix function is not difficult to understand, it is just A programming language call syntax rules, such as A to B, is actually equivalent to a.to (B).

Let’s take a look at the infix function using two concrete examples, starting with a simple example.

The String class has a startsWith() function that you must have used to determine if a String startsWith a given argument. For example, the following code must be true:

if ("Hello Kotlin".startsWith("Hello")) {
    // Handle the concrete logic
}
Copy the code

The startsWith() function is very simple to use, but with the infix function, we can express this code in a more readable syntax. Create a new infix.kt file and write the following code:

infix fun String.beginsWith(prefix: String) = startsWith(prefix)
Copy the code

First, this is an extension function of the String class, excluding the first infix keyword. We added a beginsWith() function to the String class, which is also used to determine whether a String startsWith a specified argument, and its internal implementation is the called String class’s startsWith() function.

But by adding the infix keyword, the beginsWith() function becomes an infix function, so in addition to traditional function calls, we can call beginsWith() with a special syntax sugar format, as follows:

if ("Hello World" beginsWith "Hello") {
    // Handle the concrete logic
}
Copy the code

As you can see from this example, the syntax rules for the infix function are not complicated. The code above is simply calling the beginsWith() function of the string “HelloKotlin” and passing a “Hello” string as an argument. But the infix function allows us to get rid of computer-related syntax like decimal points, parentheses, and so on in function calls, so that we can write the program in a more English syntax that makes the code more readable.

In addition, the infix function has two strict restrictions due to its special syntax sugar format. First, the infix function cannot be defined as a top-level function. It must be a member function of a class, and can be defined as an extension function to a class. Second, the infix function must accept only one parameter, and there is no limit as to the parameter type. Infix’s syntactic sugar only works if both of these things are met, and you can think about it.

Having seen the simple example, let’s look at a more complicated example. For example, if you have a set, if you want to determine whether the set contains a specified element, you can write:

val list = listOf("Apple"."Banana"."Orange"."Pear"."Grape")
if (list.contains("Banana")) {
    // Handle the concrete logic
}
Copy the code

Simple, right? But we can still make this code more readable by using the infix function. Add the following code to the infix.kt file:

infix fun <T> Collections<T>.has(element: T) = contains(element)
Copy the code

As you can see, we added an extension function to the Collection interface. This is because Collection is the overall interface for all collections in Java and Kotlin, so we added a has() function to Collection, So all subclasses of sets can use this function.

In addition, the definition of a generic function is used so that the has() function can take arguments of any concrete type. The inner implementation logic of this function is fairly simple, just a call to the contains() function on the Collection interface. In other words, the has() function does exactly the same thing as the contains() function, except that it has the syntactic sugar of the infix function by adding the infix keyword.

Now we can use the following syntax to determine whether the set contains the specified element:

val list = listOf("Apple"."Banana"."Orange"."Pear"."Grape")
if (list has "Banana") {
    // Handle the concrete logic
}
Copy the code

Ok, so now that you’ve seen both examples, you should have a good idea of the infix function. But one question you might have in mind is what is the implementation of the mapOf() function that allows you to use A to B syntax to build key-value pairs? To solve the mystery, let’s look directly at the source code for the to() function, which looks like this:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Copy the code

As you can see, the to() function is defined under type A in the same way that generic functions are defined, and takes an argument of type B. So A and B can be two different types of generics, which allows us to construct key-value pairs such as strings to integers.

Looking at the implementation of the to() function, we simply create and return a Pair. That is, A syntax structure like A to B actually yields A Pair containing data from A and B, whereas mapOf() actually receives A mutable argument list of type Pair, so we have completely decrypted this magic syntax structure.

In the spirit of hands-on practice, it’s possible to copy the source code for the to() function and write your own key-value pair builder. Add the following code to the infix.kt file:

infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)
Copy the code

We just renamed the to() function to the with() function, but the rest of the implementation logic is the same, so I don’t believe there’s any need to explain it. Now we can use the with() function to build the key pairs in our project, and we can pass the built key pairs into the mapOf() method:

val map = mapOf("Apple" with 1."Banana" with 2."Orange" with 3."Pear" with 4."Grape" with 5)
Copy the code

Isn’t that amazing? This is where the infix function brings a lot of interesting functionality, and using it flexibly can really make the syntax more readable.

Applications of higher order functions

Higher-order functions are great for simplifying calls to various apis, and some of the original uses of apis can be greatly improved, both in terms of ease of use and readability, by simplifying them with higher-order functions.

To illustrate, we’ll use higher-order functions in this section to make both the SharedPreferences and ContentValues apis easier to use.

Simplify the use of SharedPreferences

Let’s start with SharedPreferences. Before we start simplifying it, let’s review the original usage of SharedPreferences. The process of storing data in SharedPreferences can be roughly divided into the following three steps:

  1. Call the Edit () method of SharedPreferences to get the SharedPreferences.editor object;
  2. Add data to the SharedPreferences.Editor object;
  3. The data store operation is completed by calling the apply() method to commit the added data.

The following is an example of the corresponding code:

val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name"."Tom")
editor.putInt("age".28)
editor.putBoolean("married".false)
editor.apply()
Copy the code

Of course, this code is simple enough on its own, but this is more of a Java approach to writing code, and in Kotlin we can obviously do better.

Let’s try to simplify SharedPreferences using a higher-order function. Create a sharedPreference. kt file and add the following code to it:

fun SharedPreferences.open(block: SharedPreferences.Editor. () - >Unit) {
    val editor = edit()
    editor.block()
    editor.apply()
}
Copy the code

This code isn’t long, but it covers the cream of higher-order functions, which I’ll explain below.

First, we added an open function to the SharedPreferences class by extending the function, and it also takes an argument of a function type, so the open function is naturally a higher-order function.

Because the SharedPreferences context is within the open function, you can call the Edit () method directly to get the SharedPreferences.editor object. In addition, the open function receives a SharedPreferences.Editor function type parameter, so we need to call editor.block() to the function type parameter, so we can add data to the implementation of the function type parameter. Finally, the data store operation is completed by calling the editor.apply() method to submit the data.

With the open function defined, it will be easier to use SharedPreferences to store data in future projects, as follows:

getSharedPreferences("data", Context.MODE_PRIVATE).open {
    putString("name"."Tom")
    putInt("age".28)
    putBoolean("married".false)}Copy the code

As you can see, we can call the Open function directly on the SharedPreferences object and then do the data addition in the Lambda expression. Note that the Lambda expression now has the context of SharedPreferences.Editor, so you can directly call the corresponding PUT method to add data. Finally, we no longer need to call the apply() method to commit the data, because the open function does it automatically.

How about using higher order functions to simplify the use of SharedPreferences, both in terms of ease of use and readability? That’s the beauty of higher order functions. With this knowledge in mind, we can use this technique for many other apis in the future to make it easier to use.

Finally, of course, Google already provides a simplified use of SharedPreferences in the KTX extension library, This extension library is automatically included in build.gradle dependencies when Android Studio creates the project.

Therefore, we can actually store data in SharedPreferences directly in the project by writing:

getSharedPreferences("data", Context.MODE_PRIVATE).edit {
    putString("name"."Tom")
    putInt("age".28)
    putBoolean("married".false)}Copy the code

As you can see, you’re essentially replacing the open function with the Edit function, but the Edit function is significantly more semantically superior. Of course, THE main reason I named it open is to avoid having the same name as KTX’s edit function, so that you don’t get confused.

So, you might ask, why write an Open function when Google’s KTX library already has an edit function shipped with it? This is because I want you to understand higher-order functions not just at the level of use, but at the level of knowing how and why. The functionality provided in KTX is necessarily limited, but by understanding the principles behind them, you’ll be able to extend the infinite API even further.

Simplify the use of ContentValues

Now let’s learn how to simplify the use of ContentValues.

The basic usage of ContentValues was studied in Section 7.4. It is used to store and modify data in the database in conjunction with the SQLiteDatabase API.

val values = ContentValues()
values.put("name"."Game of Thrones")
values.put("author"."George Martin")
values.put("pages".720)
values.put("price".20.85)
db.insert("Book".null, values)
Copy the code

You might say that this code can be simplified using the Apply function. There’s nothing wrong with that, but we can do better.

But before we get down to business, there’s one more thing I need to tell you. Remember the use of the mapOf() function in section 2.6.1? It allows us to quickly create a key-value pair using syntax constructs like “Apple” to 1. I’m going to do A partial decrypt for you here. Using A syntactic structure like A to B in Kotlin creates A Pair object, and that’s all you need to know for now. We’ll see why in Kotlin in Chapter 9.

Once you have that knowledge, you can proceed to the next step. Create a new file contentvalues.kt and define a cvOf() method in it, as follows:

fun cvOf(vararg pairs: Pair<String, Any? >): ContentValues {

}
Copy the code

The purpose of this method is to build a ContentValues object. There are a few things I need to explain. First, the cvOf() method takes A Pair argument, which is the type of argument created using A to B syntax structure, but we prefix the argument with A vararg keyword. What does that mean? Vararg corresponds to a mutable argument list in Java. We can pass zero, one, two, or any number of arguments to this method of Pair type. These arguments will be assigned to the single variable declared by vararg. A for-in loop can then be used to iterate through all the arguments passed in.

Let’s look at the declared Pair type. Since a Pair is a key-value data structure, you need to specify what type of data its key and value correspond to via generics. The good news is that all keys in ContentValues are strings, so you can specify the generic type of the Pair key as String. But the value of ContentValues can be of many types (string, integer, floating point, or even null), so we need to specify the generic type of the Pair value as Any? . This is because Any is the common base class for all classes in Kotlin, equivalent to Object in Java, and Any? Is allowed to pass null values.

The core idea is to create a ContentValues object, iterate over the list of pairs parameters, take the data from it, and fill in the ContentValues. You just return the ContentValues object. The idea isn’t complicated, but there’s a problem: the value of the Pair argument is Any? Type, how do we make it correspond to the data types supported by ContentValues? There really isn’t a good way to do this, except to use the when statement to condition one by one and override all data types supported by ContentValues. This should be clearer when combined with the following code:

fun cvOf(vararg pairs: Pair<String, Any? >): ContentValues {
    val cv = ContentValues()
    for (pair in pairs) {
        val key = pair.first
        val value = pair.second
        when (value) {
            is Int -> cv.put(key, value)
            is Long -> cv.put(key, value)
            is Short -> cv.put(key, value)
            is Float -> cv.put(key, value)
            is Double -> cv.put(key, value)
            is Boolean -> cv.put(key, value)
            is String -> cv.put(key, value)
            is Byte -> cv.put(key, value)
            is ByteArray -> cv.put(key, value)
            null -> cv.putNull(key)
        }
    }
    return cv
}
Copy the code

As you can see, the code is basically implemented along these lines. We iterate through the list of pairs parameters using a for-in loop, fetching the key and value, and using the WHEN statement to determine the type of the value. Notice that all the data types supported by ContentValues are overwritten, and the key-value pairs passed in as arguments are added to ContentValues one by one, and ContentValues are returned.

In addition, the Smart Cast feature in Kotlin is used here. For example, when the statement enters the Int branch, the value under the condition is automatically converted to Int instead of Any? Type, so that we don’t need to make an extra downward transition like we did in Java, and this functionality applies to if statements as well.

With this cvOf() method, it becomes much easier to use ContentValues, such as inserting a piece of data into the database:

val values = cvOf("name" to "Game of Thrones"."author" to "George Martin"."pages" to 720."price" to 20.85)
db.insert("Book".null, values)
Copy the code

How’s that? Isn’t it amazing that we can now use syntactic constructs like the mapOf() function to build ContentValues?

Of course, while the cvOf() method is already very useful, it has nothing to do with higher-order functions. Because the cvOf() method takes a variable argument list of type Pair and returns an object of ContentValues, it uses no function type at all, which is inconsistent with the definition of a higher-order function.

In terms of functionality, the cvOf() approach does seem to require no knowledge of higher-order functions, but in terms of code implementation, it can be further optimized in combination with higher-order functions. For example, with the apply function, the implementation of the cvOf() method will become more elegant:

fun cvOf(vararg pairs: Pair<String, Any? >) = ContentValues().apply {
    for (pair in pairs) {
        val key = pair.first
        when (val value = pair.second) {
            is Int -> put(key, value)
            is Long -> put(key, value)
            is Short -> put(key, value)
            is Float -> put(key, value)
            is Double -> put(key, value)
            is Boolean -> put(key, value)
            is String -> put(key, value)
            is Byte -> put(key, value)
            is ByteArray -> put(key, value)
            null -> putNull(key)
        }
    }
}
Copy the code

Since the return value of the apply function is its calling object itself, we can use the syntactic sugar of the single-line function here, replacing the return value declaration with an equal sign. In addition, the context of ContentValues is automatically owned in the Lambda expression of the Apply function, so the various PUT methods of ContentValues can be called directly here. Do you find your code a little more elegant now that you have higher-order functions?

Of course, we wrote a very useful cvOf() method, but as you might have guessed, the KTX library also provides a contentValuesOf() method that does the same thing, as follows:

val values = contentValuesOf("name" to "Game of Thrones"."author" to "George Martin"."pages" to 720."price" to 20.85)
db.insert("Book".null, values)
Copy the code

The contentValuesOf() method provided by KTX can be used to write code, but this section helps you understand how to use it and how to implement it.