Chapter 6

Functional Programming in Kotlin

IN THIS CHAPTER

check Defining and understanding functional programming

check Considering the requirements of functional programming

check Performing essential tasks in functional programming

check Using functional programming for Android development

Early Android applications relied entirely on Java to perform tasks. However, Google has now embraced Kotlin as the right language to use for Android development, and with good reason — it’s often easier than Java. Throughout this book, we compare Kotlin with Java using a mix of examples to show you that Kotlin does make things easier in most cases. You can still use Java, of course, but the one thing about Kotlin that Java can’t replicate is functional programming.

This chapter isn’t about a specific programming language (even though it uses Kotlin to present examples). Instead, it’s about a programming paradigm (which, in this case, is functional programming). A paradigm is a framework that expresses a particular set of assumptions, relies on particular ways of thinking through problems, and uses particular methodologies to solve those problems. Other programming paradigms you may use are imperative, procedural, object-oriented, and declarative.

This chapter focuses on the problems you need to solve. Initially, the chapter explains how the functional programming paradigm accomplishes this problem solving, and then you see how functional programming differs from other paradigms you may have used. Later, you look at essential functional programming methods. And finally, the chapter relates functional programming techniques to Android development so that you can use this paradigm to good effect in making your code more secure, highly efficient, and easier to work with in a multiprocessing environment.

Throughout this chapter, you consider why you’d want to use functional programming at all. The math orientation of functional programming means that you might not create the essential elements of an application using it; you might instead solve straightforward math problems or devise what-if scenarios to test. As you find out in Chapter 3 of this minibook, a compiler translates every bit of code you write into machine code for execution. So, using a particular programming paradigm is a matter of making you, the developer, more efficient and better able to write code with few (or any) errors.

Defining Functional Programming

Functional programming has somewhat different goals and approaches than other paradigms do. Goals, the accomplishment of a development task in a particular manner, define what a programming paradigm is trying to do in forging the approaches used by languages that support it. For example, when working with functional programming, you express everything using mathematically based functions. Likewise, when you work with Object-Oriented Programming (OOP), the goal is to encapsulate everything in objects that reflect real-world objects as often as is possible. However, the goals of a programming paradigm don’t specify a particular implementation; doing that is within the purview of the individual languages. The following sections tell you how the functional programming paradigm differs from other paradigms in its pure state. Being in a pure state means that the language follows the paradigm guidelines precisely. As you discover later, Kotlin is an impure language, which means that it incorporates functional programming with other paradigms, such as OOP.

Differences from other programming paradigms

The main difference between the functional programming paradigm and other paradigms is that functional programs use math functions rather than statements to express ideas. So instead of writing a precise set of steps to solve a problem, you use math functions, and you don’t worry about how the language performs the task. In some ways, this approach makes languages that support the functional programming paradigm similar to applications such as MATLAB, which is used to perform various kinds of high level, complex math computations. With MATLAB, you get a user interface, which reduces the learning curve. But you pay for the convenience of the user interface with a loss of power and flexibility, which functional languages do offer. Using the functional approach to defining a problem relies on the declarative programming style, which you see used with other paradigms and languages, such as Structured Query Language (SQL) for database management.

In contrast to other paradigms, the functional programming paradigm doesn’t maintain state. The use of state enables you to track values between function calls. Other paradigms use state to produce variant results (output that changes) based on environment, such as to determine the number of existing objects and to do something different when the number of objects is zero. But because functional programs don’t maintain state, calling a functional program function always produces the same result when you have a particular set of inputs. Functional programs are therefore more predictable than those that support state.

Because functional programs don’t maintain state, the data they work with is also immutable, which means that you can’t change it. To change a variable’s value, you must create a new variable. Again, this feature makes functional programs more predictable than other approaches and could make functional programs easier to run on multiple processors.

Understanding its goals

Imperative programming, the kind of programming that most developers have done until now, is akin to an assembly line, where data moves through a series of steps in a specific order to produce a particular result. The process is fixed and rigid, and the person implementing the process must build a new assembly line every time an application requires a new result. Object-oriented programming (OOP) simply modularizes and hides the steps, but the underlying paradigm still works on that assembly-line idea. Even with modularization, OOP often doesn’t allow rearrangement of the object code in unanticipated ways because of the underlying interdependencies of the code.

Remember Functional programming gets rid of the interdependencies by replacing procedures with pure functions, which require the use of immutable state. The assembly line is no more. An application can manipulate data using the same means as those used in pure math. The seeming restriction of immutable state lets anyone who understands the math of a situation also create an application to perform the math.

Using pure functions creates a flexible environment in which code order depends on the underlying math. That math models a real-world environment, and as the human understanding of that environment changes and evolves, the math model and functional code can change with it — without the usual problems of brittleness that cause imperative code to fail. Modifying functional code is faster and less error prone than using other paradigms because the person implementing the change must understand only the math; there’s no need to know how the underlying code works. In addition, learning how to create functional code can be faster as long as the person understands the math model and its relationship to the real world.

Functional programming also embraces a number of unique coding approaches, such as the capability to pass a function to another function as input. This capability enables you to change application behavior in a predictable manner that isn’t possible using other programming paradigms.

Understanding Pure and Impure Languages

Languages that support functional programming fall into two categories: pure and impure. A pure language allows only functional programming techniques and fully implements the functional programming paradigm. An impure language allows the use of other programming techniques and may only mostly implement the functional programming paradigm. Both pure and impure languages have specific advantages and disadvantages, as described in the sections that follow.

Using the pure approach

Programming languages that use the pure approach to the functional programming paradigm rely on lambda calculus principles, for the most part. In addition, a pure-approach language allows the use of functional programming techniques only, so the result is always a functional program. Haskell is probably the most popular pure language because it provides the purest implementation and is generally a relatively popular language, according to the TIOBE index (https://www.tiobe.com/tiobe-index/). Other pure-approach languages include Lisp, Racket, Erlang, and OCaml.

When considering functional programming, especially in the pure sense, a language must meet these requirements:

  • It uses pure functions. A pure function always produces precisely the same result for a given set of inputs. Also, a pure function doesn’t modify any global variables or any input arguments. Pure functions are extremely easy to debug because they have no side effects; nothing is hidden. Plus, a compiler can do things like memorize the results of a pure function call because such calls never change, and the compiler can wait to calculate results until they’re actually needed. When working with a pure function, you can
    • Remove the function if no other functions rely on its output.
    • Reverse the order of calls to different functions without any change to application functionality.
    • Process the function calls in parallel without any consequence.
    • Evaluate the function calls in any order, assuming that the entire language doesn’t allow side effects.
  • It relies on recursion. There are no for, while, do, or any other sort of looping mechanisms in a functional language. Instead, a functional language relies on recursion because recursion doesn’t require state (a counter variable as a minimum) to complete a looping task.
  • It uses referential transparency. The language uses no assignment statements. The value of a variable is replaceable with its actual value at any point in the program. If a new value needs to be stored, the environment creates a new variable to do it.
  • Its functions are first class and can be higher order. A first-class function can act as a variable and you can pass it to any function requiring an argument. In addition, a function can return a first-class function instead of a value. Higher-order functions can accept other functions as input and output functions as results.
  • Its variables are immutable. The value of something can’t change, so you can always rely on it to be the same value. This means you can create a variable in one place and use its value in another place without ever worrying about the value of the variable changing.

Warning As with many elements of programming, people have strong opinions about whether a particular programming language qualifies for pure status. For example, many people consider JavaScript to be a pure language, even though it’s untyped. Others feel that domain-specific declarative languages such as SQL and Lex/Yacc qualify for pure status even though they aren’t general programming languages. Simply having elements of functional programming doesn’t mean that a language adheres to the pure approach.

Using the impure approach

Many developers have come to see the benefits of functional programming. However, they also don’t want to give up the benefits of their existing language, so they use a language that mixes functional features with one of the other programming paradigms (as described in the “Comparing the Functional Paradigm” section that follows). For example, you can find a few functional programming features in languages such as C++, C#, and Java.

Warning Adding functional features to an existing language rarely results in a good mix of functional features because the additions have too many compromises. When you work with an impure language, you need to be careful because your code won’t work in a purely functional manner, and the features that you might think will work in one way actually work in another. For example, you can't pass a function to another function in some languages, but doing so is a requirement for functional programming.

Tip Languages such as Kotlin and Python are designed from the outset to support multiple programming paradigms. In fact, some online courses make a point of teaching this particular aspect of Kotlin as a special benefit (see https://functionalkotlin.com/). The use of multiple programming paradigms makes Kotlin quite flexible but also leads to complaints and apologists. This chapter relies on Kotlin to demonstrate the impure approach to functional programming because it’s both popular and flexible, plus it’s easy to learn.

Comparing the Functional Paradigm

You might think that only a few programming paradigms exist besides the functional programming paradigm explored in this chapter, but the world of development is packed with them. That's because no two people truly think alike. Each paradigm represents a different approach to the puzzle of conveying a solution to problems by using a particular methodology while making assumptions about things like developer expertise and execution environment. You can find entire sites that discuss the issue, such as the one at https://cs.lmu.edu/~ray/notes/paradigms/. Oddly enough, some languages (such as Kotlin and Python) mix and match compatible paradigms to create an entirely new way to perform tasks based on what has happened in the past. Here’s a progression to think about as you work with impure functional languages:

  • Imperative: Imperative programming takes a step-by-step approach to performing a task. The developer provides commands that describe precisely how to perform the task from beginning to end. During the process of executing the commands, the code also modifies application state, which includes the application data. The code runs from beginning to end. An imperative application closely mimics the computer hardware, which executes machine code. Machine code is the lowest set of instructions that you can create and is mimicked in early languages, such as assembler.
  • Procedural: Procedural programming implements imperative programming, but adds functionality such as code blocks and procedures for breaking up the code. The compiler or interpreter still ends up producing machine code that runs step by step, but the use of procedures makes it easier for a developer to follow the code and understand how it works. Many procedural languages provide a disassembly mode in which you can see the correspondence between the higher-level language and the underlying assembler. Examples of languages that implement the procedural paradigm are C and Pascal.
  • Object-oriented: The procedural paradigm does make reading code easier. However, the relationship between the code and the underlying hardware still makes it hard to relate what the code is doing to the real world. The object-oriented paradigm uses the concept of objects to hide the code, but the more important aim is to make modeling the real world easier. A developer creates code objects that mimic the real-world objects they emulate. These objects include properties, methods, and events to allow the object to behave in a particular manner. Examples of languages that implement the object-oriented paradigm are C++ and Java.

    Remember Languages that implement the object-oriented paradigms also implement both the procedural and imperative paradigms. The fact that objects hide the use of these other paradigms doesn’t mean that a developer hasn’t written code to create the object using these older paradigms. Consequently, the object-oriented paradigm still relies on code that modifies application state, but could also allow for modifying variable data.

  • Declarative: Functional programming actually implements the declarative programming paradigm, but the two paradigms are separate. Other paradigms, such as logic programming, implemented by the Prolog language, also support the declarative programming paradigm. The short view of declarative programming is that it does the following:
    • Describes what the code should do, rather than how to do it
    • Defines functions that are referentially transparent (without side effects)
    • Provides a clear correspondence to mathematical logic

Using Kotlin for Functional Programming Needs

Remember that functional programming is a paradigm, which means that it doesn’t have an implementation. The basis of functional programming is lambda calculus (https://brilliant.org/wiki/lambda-calculus/), which is actually a math abstraction. So when you want to perform tasks by using the functional programming paradigm, you're really looking for a programming language that implements functional programming in a way that meets your needs. In fact, you may even be performing functional programming tasks in your current language without realizing it. Every time you create and use a lambda function, you’re likely using functional programming techniques (in an impure way, at least).

Kotlin has significantly more functional features than Java does, but Kotlin still shows its Java roots. For purists, the Java connection is a problem because Kotlin code can look too much like Java code (as shown in Chapter 5 of this minibook). Developers often look at languages based on need, rather than on strict adherence to a set of principles. Still, you can find discussions online that say Kotlin isn’t quite as functional as Python and not nearly as functional as languages such as Haskell. These theoretical discussions don’t actually matter in the real world. You want to use Kotlin for Android development when functional programming is important because Kotlin

  • Is suitable for Android and possibly system development, whereas Python focuses on data science and machine learning needs
  • Can execute relatively quickly, even on devices that have less robust processors
  • Uses a smaller number of lines of code when compared to Java due to the ability to use functional techniques
  • Is easier to read than Java code
  • Is easier to learn than Java, but not as easy as Python
  • Offers multi-statement lambdas
  • Has relatively compact collection comprehensions, which improve readability of collection manipulations
  • Uses receivers to create complex code manipulations with simple names
  • Has access to inline functions that actually do place the function into the surrounding code, making the application run faster (at the cost of size)
  • Includes special functions to handle potentially null input

With Kotlin, you gain all these features that require some level of functional programming without losing access to the Java libraries. Because Java libraries are important for Android development, an Android developer should see Kotlin functional capabilities as being more useful than Java.

Tip When working with Kotlin, you also get a bonus for Android development; you can use C++ libraries, too. The articles at https://yalantis.com/blog/android-ndk-the-interaction-between-kotlin-and-c-c-plus-plus/ and https://developer.android.com/studio/projects/add-native-code tell how to use these libraries. Given how Kotlin works, you can use C++ to augment your applications in a number of ways, especially when it comes to obtaining the last bit of speed from your application.

Defining the Role of State

Application state is a condition that occurs when the application performs tasks that modify global data. An application doesn't have state when using functional programming. The good news about the lack of state is that any call to a function will produce the same results for a given input every time, regardless of when the application calls the function. But here’s the bad news: The application now has no memory. When you think about state, think about the capability to remember what occurred in the past, which, in the case of an application, is stored as global data.

Kotlin naturally works to keep the role of state in applications to a minimum; although state does sometimes make an appearance in Android programming. For example, creating views without some sort of state information would be hard. The way Android handles events also makes managing state necessary, but you can still reduce the amount of state management in your applications and obtain a better result than using OOP or procedural techniques alone. Here are some things to consider:

  • Variables can be made immutable by default, and this is actually the preferred method of creating them.
  • Variables are non-nullable by default, which means that they always have a result to provide.
  • Classes are final by default, making it possible to depend on specific class behaviors.
  • Currying allows you to change complex functions that require multiple arguments into a series of simple functions that require just one argument each.
  • Data structures are immutable by default, which means that you can depend on them to remain the same after creation.
  • Working with data using pure functions is not only possible but also efficient and concise.

Remember State is unavoidable for many reasons in real-world applications. For example, you can’t stream data without creating both state and side effects. This is where functional programming falls short of providing a complete solution. If you want to stream your favorite music using an app you create, your app will have to store and manipulate state.

Using Recursion to Perform Calculations

The “Looping using Kotlin recursion” section of Chapter 4 of this minibook tells you how you can use recursion to perform looping tasks. The main benefit of this approach is that you can create loops that have no state. Also, recursion can be an elegant way to solve certain problems. Still, using recursion can be like bending your head around a hole in time and space — it’s not particularly easy because it takes a different mindset to perform.

The more common way to use recursion is to perform calculations. In fact, some very common math problems, such as the calculation of factorial (n!) and the Fibonacci sequence (Fn) are solved using recursion. Kotlin supports two forms of recursion for solving problems: standard and tail recursion. The following sections discuss both kinds of recursion.

Relying on standard recursion

When using recursion to solve a problem, you normally start with something complex that can be divided into a simple case. For example, when you calculate a factorial, 1!, is the simplest case because it has a value of 1. So, this simple case is n = 1. The next case is a little harder; now you must multiply 1 * 2 to obtain the result, so this next case is n = 1 * 2. Instead of just one step, you now have two steps. Each case becomes progressively more difficult, with 3 being the next case: n = 1 * 2 * 3. So, recursion rests on dividing the problem into simple steps until you reach the simplest case.

The reason recursion works for the factorial problem is that you can create this reduction in complexity by having the function call itself as often as needed to reach the simplest case of n = 1. (If you want to play with the code that follows and in other areas of this chapter without having to start Android Studio, you can use an online interpreter, such as the one at https://try.kotlinlang.org/.) Here is the Kotlin code to perform this task:

fun main(args: Array<String>) {
println(factorial(5))
}

fun factorial(n: Int): Int {
if (n == 1){
println("Ended")
} else {
println("Handling n = $n")
var value = n * factorial(n - 1)
println("Value = $value")
return value
}

return n
}

This code contains a number of println() statements so that you can better understand how the recursion works. Normally, this code could be extremely short:

fun fact(n: Int): Int {
return if (n==1) n else n * fact(n - 1)
}

You could even make it shorter still, but really, it's short enough. The output from the wordy version looks like this:

Handling n = 5
Handling n = 4
Handling n = 3
Handling n = 2
Ended
Value = 2
Value = 6
Value = 24
Value = 120
120

The factorial() function keeps calling itself until it reaches the simplest case, n == 1. However, it doesn't finish anything. It doesn’t get to the next step, which is var value = n * factorial(n - 1). At each step, the code pushes the current iteration onto the stack (think of a stack of pancakes, except we’re talking memory here) and then it calls the next iteration and finally reaches n == 1. This is why you see each of the Handling n = statements as output without any value statements.

At this point, the procedure for n == 1 finishes and this iteration, n == 1, is popped from the stack, which means the next iteration, n == 2 * 1, can complete. This is why you see an output of Value = 2. Each iteration completes and is popped from the stack in turn, until, finally, nothing is left to return and the factorial() function returns the value of n == 5 * 4 * 3 * 2 * 1, which is 120.

Remember The main benefit of this approach to recursion is that it works on every kind of problem. Developers have used this form of recursion for years with every language imaginable, so it's hardly an earth-shaking approach to dazzle your friends. Consider this the tried-and-true approach to recursion.

Relying on tail recursion

You might wonder why someone would need a second form of recursion given that standard recursion will work on every problem. The fact is that standard recursion has certain problems that you can solve using this second form, which is called tail recursion because there is nothing to do after the last step of the recursion process. Using tail recursion has these advantages:

  • It executes faster because it doesn’t rely on stack pushes and pops to maintain the previous iteration.
  • Recursion can occur infinitely because no stack resources are used (so there is no chance of running out of memory due to the recursion).
  • It eliminates the potential for StackOverflow exceptions.

You have a few ways to handle tail recursion. Some don’t work with every problem, but you can take a look at a general approach before delving into other approaches. Here is a version of the factorial problem using tail recursion (use the main() function from the “Relying on standard recursion” section to execute both factorial2() and factorial3()):

fun factorial2(n: Int, acc: Int = 1): Int {
if (n == 1) {
println("Ended with n = $n, acc = $acc")
return acc
}

println("Handling n = $n, acc = $acc")
return factorial2(n - 1, acc * n)
}

In this case, the code relies on an accumulator, acc, to track the current value. Notice that the recursive call occurs at the tail — the end — of the process. The actual process is similar, but you won't encounter stack problems using this approach. Here is the output from this version of the example:

Handling n = 5, acc = 1
Handling n = 4, acc = 5
Handling n = 3, acc = 20
Handling n = 2, acc = 60
Ended with n = 1, acc = 120
120

Remember The only problem with factorial2() is that it hasn’t been optimized to use tail recursion. You won’t actually see this optimization as part of the code (you write it the same way), but you need to tell Kotlin to perform the optimization for you using the tailrec keyword. Here is a final version that won't suffer from stack overflows and works quite fast:

tailrec fun factorial3(n: Int, acc: Int = 1): Int {
return if (n == 1) acc else factorial3(n - 1, acc * n)
}

Warning The only problem with tail recursion is that it won’t work with every problem. You need to validate the results of any recursion you perform using this approach to ensure that you’re getting the correct output.

Using Function Types

Creating a function that acts as a type is one of the tenets of functional programming. Kotlin provides a number of methods to use function types. Naturally, first you have to start with one or two functions that you want to use, such as these shown here:

fun doAdd(a: Int, b: Int): Double = a.toDouble() + b
fun doSub(a: Int, b: Int): Double = a.toDouble() – b

There really isn’t anything special about these functions except they output a Double as the result of Int math. However, they serve well to demonstration how function types could be helpful in Android programming.

The first way to use these functions as types is to create a variable that is equal to the function. You can then invoke the variable, as shown in the following example:

val f = ::doAdd
println(f(1, 2))

Notice the function reference (::) operator. This operator tells Kotlin to reference the function in variable f. You can then invoke f(1, 2) as part of a println() call. However, you can use f anywhere that you use a variable. For example, you can place the value into another variable, as shown here:

val myNum = f(1, 2)
println("${myNum::class.qualifiedName}")

The output type of myNum is a Double, as you'd expect, not a function. You can use f anywhere you need a Double, but because of the nature of f, the value you receive depends on the inputs you provide.

You can also use a function type as input to another, higher-order, function. For example, consider this higher-order function:

fun doMath(MathType: (a: Int, b: Int) -> Double ,
a: Int, b: Int): Double {
return MathType(a, b)
}

The first argument, MathType, relies on a function input with a particular signature, one that matches the two example functions. You place the input arguments within parentheses as usual, followed by ->, and then the output type. The doMath() function accepts any function that has the correct signature, so you can do this in your code:

println(doMath(::doAdd, 2, 1))
println(doMath(::doSub, 2, 1))

Remember The output you receive depends on the function you provide as input. Notice that you must use the function reference operator in this case as well or the compiler will complain.

Understanding Function Literals

A function literal is an extremely compact type of function that you can supply directly to functions as an argument. Unlike most languages, Kotlin allows multistatement function literals, but the final statement must always provide the return value. There are two kinds of function literal: lambda expressions and anonymous functions. The following sections explore both forms.

Lambda expressions

A lambda expression is an expression that you assign to a variable. You can make the assignment directly, or you can make the assignment to a function argument. Here's an example of a lambda expression assigned to a variable:

val doAdd = {a: Int, b: Int -> a.toDouble() + b}
println(doAdd(1, 2))

You can also supply a lambda expression as input to a function that expects a function as input. Consider these string manipulation examples:

fun main(args: Array<String>) {
val repeat: String.(Int) -> String =
{times -> this.repeat(times)}
val part: String.(Int) -> String =
{len -> this.take(len)}

println(changeString(repeat, "A String ", 4))
println(changeString(part, "A String ", 4))
}

fun changeString(function: (String, Int) -> String,
string: String, number: Int): String {
return function(string, number)
}

Tip This example has two different string manipulation lambda functions with an important change from the previous example. Notice the String.(Int) part of the declaration. The String is referenced as the this argument in the lambda function. So, this.repeat or this.take refers to the String that is supplied as input. Using this technique can make your coding easier at times.

Another difference is that this example provides an expression signature so that it's clear what the expression requires as input. Normally, the compiler automatically detects the signature, but defining one doesn’t hurt. The repeat signature of String.(Int) -> String states that the expression needs a String that is used for this, along with an Int, and it outputs a String.

Remember Notice that only the Int appears as an argument to the expression in the form of times: {times -> this.repeat(times)}. When working with a lambda expression of this sort, developers have a tendency to provide too many or too few inputs to the expression.

The function, changeString(), works differently depending on the lambda expression you provide. Using this approach means that you can write a single function that performs what is essentially a boilerplate task of manipulating a string, but it can change the behavior of the function based on what you need at any given time. As long as the signature of the function matches the signature of the lambda expression, the function will work as anticipated.

Anonymous functions

An anonymous function is simply a function that lacks a name. You use anonymous functions in many of the same places as you do lambda expressions. In many cases, the choice between the two comes down to a combination of personal preference and clarity of expression. Even though the two forms of expression obtain the same result in most cases, the way you construct the expression differs, so one form might be clearer in some cases than the other is.

Remember Aside from the need to declare an anonymous function as a function (despite it acting like an expression), an anonymous function also differs from a lambda expression in one important way: You can provide a return type with an anonymous function.

The previous section shows how to use multiple lambda expressions with a single function, changeString(). You can perform the same task using an anonymous function, as shown here:

fun main(args: Array<String>) {
val repeat = fun(inStr: String, times: Int): String {
return inStr.repeat(times)
}
val part = fun(inStr: String, len: Int): String {
return inStr.take(len)
}

println(changeString(repeat, "A String ", 4))
println(changeString(part, "A String ", 4))
}

fun changeString(function: (String, Int) -> String,
string: String, number: Int): String {
return function(string, number)
}

Notice that changeString() hasn't changed at all, and this is the amazing thing about being able to use either a lambda expression or an anonymous function. The function receiving either form doesn’t change, which means that even when you provide expressions to Kotlin or Android functions, you still have a choice as to which form to use for your particular need.

The anonymous function looks like any other function you might have created in the past, except for the lack of a name. You can create a function body of any size needed to complete the task, so anonymous functions often work better than lambda expressions when performing a complex set of steps within the function body.

Defining the Function Types

When using functional programming techniques in Kotlin, you discover that Kotlin has some built-in function types. Each of these function types reduces the code needed to perform a task, while also making the code clearer and easier to understand. The following sections discuss the most important function types and demonstrate how to use them. These functions can be quite important in Android programming in helping manipulate data for various purposes.

Comprehensions

A comprehension is a kind of manipulation you perform on a List, Set, or Map to understand it in a certain way. These comprehensions are listed as part of the extension functions for a collection, such as those shown for List at https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html. To differentiate a comprehension from another sort of extension function, look at the input, which is normally a predicate or transform, and the output, which is normally a subset of the current List.

A comprehension begins with the original List, followed by the dot-separated list of comprehension functions. Here's an example of a numeric comprehension that returns only the values that meet a specific criterion and transforms them into strings:

val numbers = listOf(1, 2, 3, 4, 5)
val selection = numbers
.filter {it >= 3}
.map {"Value = $it"}

selection.forEach({println(it)})

Tip Placing each comprehension on its own line tends to make the entire expression easier to read. In this case, the filter() function returns only those numbers elements that are equal to or greater than 3. It then calls on map() to transform the numbers into a specifically formatted string. Each function works on the input List in the order in which it appears, so order is important in defining how the comprehension works.

Remember Whenever you work with comprehensions, it represents the collection as it appears from the previous step. So, in .filter {it >= 3}, it represents the original List found in numbers. However, in .map {"Value = $it"}, it represents the modified List from the filter step. Here's the output from this example:

Value = 3
Value = 4
Value = 5

Receivers

A receiver is a kind of member addition to a class made outside the class proper. You use a receiver as the start of an extension function that makes the class able to do more than it otherwise could without actually modifying the class itself or creating a new subclass. The receiver consists of a signature followed by a lambda expression. Here is a simple example:

class Name (val aName: String)

fun main(args: Array<String>) {
val greet: Name.() -> String =
{"Hello ${this.aName}!"}

val person = Name("George")

println(person.greet())
}

The example begins with the Name class, which takes a String argument, aName, as part of its primary constructor. The receiver, greet, references the Name class and provides an extension to it (as designated by the period) that requires no arguments (as shown by the empty parentheses). The lambda expression outputs a string that includes the content of aName. Note that you must reference aName using this.aName.

To use the extension, you create a new object, person, and provide a value of "George" to aName. The call to println() displays the greeting Hello George! by calling person.greet().

You can further extend this example by providing a specific greeting, rather than simply Hello. This update allows the addition of a greeting:

val greet: Name.(String) -> String =
{"$it ${this.aName}!"}

val person = Name("George")

println(person.greet("Good morning"))

Remember In this case, you specify a String input argument for greet(). A receiver can only have one input parameter. You reference this input argument using it within the lambda expression code. In addition, when calling greet(), you must now supply the input argument. Consequently, this version of the code outputs: Good morning George!.

One of the reasons that knowing how to use receivers and extension functions is so important is that some Kotlin classes and functions use them. For example, even though you're usually not likely to write code like this, you could create a complex StringBuilder object using this approach:

val aGreeting = StringBuilder("Hello ")
.apply {
append("there ")
append("from Kotlin!")
}.toString()

println(aGreeting)

The apply() inline function accepts a series of inputs to modify the original StringBuilder input using the append() extension function. You can see a list of other StringBuilder extension functions at https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-string-builder/index.html. The output of this example is Hello there from Kotlin!.

Inline

When you create lambda expressions, anonymous functions, standard functions, and other kinds of modularized code, the code resides in a separate area of memory. To use the code, the compiler must create a connection to it, which adds overhead to the application. If you call this modularized code many times, the overhead can significantly slow your application, but at the same time, because the code exists in just one place, you don't use any additional memory. On the other hand, if you were to repeat the modularized code everywhere you need it, application performance would improve, but at the cost of memory. (Not only that, but by repeating the code all over the place you make code modifications a nightmare!)

Remember A way around the performance issue is to use the inline keyword to tell the compiler to inline the code — that is, to place the compiled version of the code directly where the application needs it. For example, you could inline this code:

fun main(args: Array<String>) {
val repeat = fun(inStr: String, times: Int): String {
return inStr.repeat(times)
}
val part = fun(inStr: String, len: Int): String {
return inStr.take(len)
}

println(changeString(repeat, "A String ", 4))
println(changeString(part, "A String ", 4))
}

inline fun changeString(function: (String, Int) -> String,
string: String, number: Int): String {
return function(string, number)
}

When you inline the code like this, the println() calls change. What the compiled version does looks like this:

println(("A String ").repeat(4))
println(("A String ").take(4))

Utility

Kotlin provides a number of utility functions that make dealing with potentially null values a lot easier. In some cases, you use them to manipulate values in such a manner that potentially null results aren't a problem. Here is a list of utility functions that you should keep in mind:

  • run(): Enables you to work with potentially null values and pass non-null values to functions that can’t accept a null value. Here’s an example of run() in use:

    fun main(args: Array<String>) {
    val myInt: Int? = null
    val result = myInt?.run {greaterThan(this)}
    println(result)
    }

    fun greaterThan(x: Int): Boolean {
    return x > 5
    }

    In this case, run() will place a value of null in result because myInt is null. However, if myInt hadn't been null, result would contain the Boolean comparison provided by greaterThan().

  • let(): This is a syntactical variation of run(), except that you can use it with a series of normal functions rather than function types with receivers. A difference between run() and let() is in the passing of values. You'd need to change the call in the previous example from this (an object) to it (a value), like this:

    val result = myInt?.let {greaterThan(it)}

  • with(): Performs one or more operations on a non-null variable. To modify the previous example to use with(), you need to make myInt non-null, like this:

    val myInt: Int = 5
    val result = with(myInt) {greaterThan(this)}

  • apply(): Applies extension functions when provided with a non-null value. If the value is null, the output is null as well. This form uses the object this to pass values to the functions.
  • also(): Applies normal functions when provided with a non-null value. If the value is null, the output is null as well. This form uses the object it to pass values to the functions.
  • takeIf(): Performs a conditional comparison and, if the comparison is true, passes value to a lambda expression. Here's an example:

    val myInt: Int = 5
    val result = myInt.takeIf {it > 4} ?.let {it * it}
    println(result)

    In this case, the code determines whether myInt is greater than 4. If so, the code returns the square of the value in myInt. The variable being compared can't be null. When the comparison fails, the output is a null value.

  • takeUnless(): Performs the opposite of takeIf(). The result is non-null when the comparison fails.

Using Functional Programming for Android Apps

Throughout this chapter you have discovered what functional programming is and how to perform tasks using it, which is nice, but it's not helpful if you don’t have a practical use for it when building an Android app. The world is packed with interesting technologies that don’t necessarily work well or aren’t practical. Some are just downright useless, even though they started out looking like a promising approach, like the NullPointerException (see Chapter 3 of this minibook for a discussion of this particular issue). From a general perspective, using functional programming techniques has these advantages in nearly any environment in which you use Kotlin:

  • Reduction of code size
  • Easier to debug
  • Faster in most cases
  • Less prone to side effects
  • Easier to read and understand
  • More flexible
  • Less potential for data loss or corruption
  • Reduction of security issues
  • Fewer errors because of better compiler support

However, you haven’t really seen functional techniques used for any of the book examples so far because the book examples have focused on the essential Android environment where the Java influence is felt quite strongly. The examples in this chapter give you a start by showing various techniques, but they’re outside the Android environment. As you move into more complex examples, such as those found in Book 4, you start to see the potential for using functional programming techniques.

Tip You can also view some detailed articles online, such as “A Functional Approach to Android Architecture using Kotlin,” by Jorge Castillo, at https://academy.realm.io/posts/mobilization-2017-jorge-castillo-functional-android-architecture-kotlin/. In this article, you discover how functional programming techniques help fix some of the issues that Java developers encountered when writing code for apps like games. The example code looks at a simple task — fetching a hero — and explores the serious problems involved in doing it in Java and even in Kotlin without functional programming approaches.

Comparing languages is always an opinionated process because developers simply like particular languages, sometimes for no apparent reason except that the language of choice is familiar. Looking at use cases, however, could make it easier to determine how to use Kotlin and functional programming techniques to make your next app a success. The article “Kotlin vs. Java: Which Programming Language to Choose for Your Android App,” by Maria Redka, at https://mlsdev.com/blog/kotlin-vs-java takes a use-case approach to comparing the two languages and the techniques they rely on to get the job done.

Warning Functional programming may not work well for every development team, especially if you’re already heavily invested in Java. However, you can find case studies online, such as “Why we choose Kotlin for creating Android apps,” by Dima Kovalenko, at https://hackernoon.com/why-we-choose-kotlin-for-creating-android-apps-46030b10d19c. This article discusses experiences in moving from Java to Kotlin. The article writer isn’t shy in saying that some things simply didn’t work at the outset. Reviewing articles of this sort can help you understand whether functional programming techniques will work in your particular case (something that would be very tough to do in a book).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.75.227