Chapters

Hide chapters

Kotlin Apprentice

Second Edition · Android 10 · Kotlin 1.3 · IDEA

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

21. Functional Programming
Written by Irina Galata

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

The evolution of programming as an engineering discipline includes improvement of languages and tools. There are also different fundamental approaches you can use to develop your programs, often called paradigms. There are various programming paradigms, but don’t be intimidated, since all of them have strengths and weaknesses.

The more comfortable you become with the different approaches, the easier it will be able to apply them. Plus, you already know at least one of them: object-oriented programming aka OOP. In this chapter, you’ll get acquainted with another type of programming — functional programming — and learn its technical details.

What is functional programming?

You may remember that a key feature of OOP is that classes and their instances contain properties and methods. Functional programming is instead based around the use of functions, which ideally don’t have side effects.

A side effect is any change to the state of a system. A simple example of a side effect is printing something to the screen. Another is changing the the value of the property of an object. Side effects are typically rampant in OOP, as class instances send messages back and forth to one another and change their internal state.

Another key feature of functional programming making functions first-class citizens of the language, as you’ll see in a future section.

Most functional programming languages also rely on a concept called referential transparency. This term effectively means that given the same input, a function will always return the same output. When a function has this property it is called a pure function.

Functions in functional programming languages are more like their mathematical namesake functions than typical functions in non-functional programming approaches.

Unlike versions of Java prior to Java 8, Kotlin allows you to use both the OOP and functional approaches to building software, either separately or by combining them to make your code more efficient and flexible. In this chapter, you’ll see an example of the combination of the ideas of OOP and functional programming.

Robot battle!

Before diving into using functional programming, we’ll setup a system that will let us explore the details.

import java.util.*

class Robot(val name: String) {
  private var strength: Int = 0

  private var health: Int = 100
  
  init {
    strength = Random().nextInt(100) + 10
    report("Created (strength $strength)")
  }
  
  fun report(message: String) {
    println("$name: \t$message")
  }
}
// 1
var isAlive: Boolean = true

// 2
fun attack(robot: Robot) {
  // 3
  val damage = (strength * 0.1 + Random().nextInt(10)).toInt()
  
  // 4
  robot.damage(damage)
}

private fun damage(damage: Int) {
  // 5
  val blocked = Random().nextBoolean()

  if (blocked) {
    report("Blocked attack")
    return
  }

  // 6
  health -= damage
  report("Damage -$damage, health $health")

  // 7
  if (health <= 0) {
    isAlive = false
  }
}
object Battlefield {
  // 1
  fun beginBattle(firstRobot: Robot, secondRobot: Robot) {
    // 2
    var winner: Robot? = null
    // 3
    battle(firstRobot, secondRobot)
    // 4
    winner = if (firstRobot.isAlive) firstRobot else secondRobot
  }

  fun battle(firstRobot: Robot, secondRobot: Robot) {
    // 5
    firstRobot.attack(secondRobot)
    // 6
    if (secondRobot.isAlive.not()) {
      return
    }
    // 7
    secondRobot.attack(firstRobot)
 
    if (firstRobot.isAlive.not()) {
      return
    }
    // 8
    battle(firstRobot, secondRobot)
  }
}
val firstRobot = Robot("Experimental Space Navigation Droid")
val secondRobot = Robot("Extra-Terrestrial Air Safety Droid")

Battlefield.beginBattle(firstRobot, secondRobot)

First-class and higher-order functions

One of the main ideas of functional programming is first-class functions. This means that you can operate with functions in the same ways you can other elements of the language — you can pass functions as arguments to other functions, return functions from functions, and assign functions to a variable. Functions that receive a function as a parameter or return functions are called higher-order functions.

Function types

To declare a function which receives a function parameter or returns another function, it’s necessary to know how to specify a function type.

Passing a function as an argument

Let’s update beginBattle() to receive another function as a parameter, which will be executed when the battle is finished. That way, we’ll know exactly which robot has won.

fun beginBattle(firstRobot: Robot, secondRobot: Robot,
                onBattleEnded: (Robot) -> Unit) {
  var winner: Robot? = null
  battle(firstRobot, secondRobot)
  winner = if (firstRobot.isAlive) firstRobot else secondRobot
  onBattleEnded(winner)
}
fun main(args: Array<String>) {
  val firstRobot = Robot("Experimental Space Navigation Droid")
  val secondRobot = Robot("Extra-Terrestrial Air Safety Droid")
  Battlefield.beginBattle(firstRobot, secondRobot, ::onBattleEnded)
}

fun onBattleEnded(winner: Robot) {
  winner.report("Won!")
}

Returning functions

Similar to passing functions as arguments, you can return a function from another function, as in the following code:

fun someFunction(): () -> Int {
  return ::anotherFunction
}

fun anotherFunction(): Int {
  return Random().nextInt()
}

Lambdas

As you learned about in Chapter 10, a lambda is a function literal, which can be invoked, passed as an argument or returned just like ordinary functions. In this chapter, you’ll learn a bit more about lambdas in the context of functional programming.

val pow = { base: Int, exponent: Int -> Math.pow(base.toDouble(), exponent.toDouble()) }
pow(2, 4)
val pow: (Int, Int) -> Double
    = { base, exponent -> Math.pow(base.toDouble(), exponent.toDouble()) }
val root: (Int) -> Double = { Math.sqrt(it.toDouble()) }
fun main(args: Array<String>) {
  val firstRobot = Robot("Experimental Space Navigation Droid")
  val secondRobot = Robot("Extra-Terrestrial Air Safety Droid")
  val onBattleEnded = { winner: Robot -> winner.report("Won!") }
  Battlefield.beginBattle(firstRobot, secondRobot, onBattleEnded)
}
fun main(args: Array<String>) {
  val firstRobot = Robot("Experimental Space Navigation Droid")
  val secondRobot = Robot("Extra-Terrestrial Air Safety Droid")
  Battlefield.beginBattle(firstRobot, secondRobot) {
    it.report("Won!")
  }
}
{ it.report("Won!") }

How do lambdas work?

When you defined the onBattleEnded lambda, it was compiled to the equivalent of the following Java code.

final class MainKt$main$onBattleEnded$1 extends Lambda
        implements Function1 {

public static final MainKt$main$onBattleEnded$1 INSTANCE =
            new MainKt$main$onBattleEnded$1;

public bridge invoke(Object arg0) {
  MainKt$main$onBattleEnded$1.invoke((Robot)arg0);
}

public final invoke(Robot robot) {
  robot.report("Won!");
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
  /** Invokes the function with the specified argument. */
  public operator fun invoke(p1: P1): R
}
Function1 onBattleEnded =
            (Function1)MainKt$main$onBattleEnded$1.INSTANCE;
onBattleEnded.invoke(winner);

Closures

Lambas (as well as local functions) act as closures, which means that they can access and modify variables defined outside of their own scope. Unlike Java, variables declared in the outer scope can be modified within the closure.

var result = 0

val sum = { a: Int, b: Int ->
  result = a + b
}

sum(5, 18)
final IntRef result = new IntRef();
result.element = 0;
Function2 sum = (Function2)(new Function2() {
  public Object invoke(Object var1, Object var2) {
    this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
    return Unit.INSTANCE;
  }

  public final void invoke(int a, int b) {
    result.element = a + b;
  }
});
sum.invoke(Integer.valueOf(5), Integer.valueOf(18));
public static final class IntRef implements Serializable {
  public int element;

  @Override
  public String toString() {
    return String.valueOf(element);
  }
}

Extension functions

You learned about extension methods on classes in Chapter 14. Let’s look at them again from the perspective of functional programming.

fun String.print() = System.out.println(this)
val string = "Hello world"
string.print()
public static final void print(@NotNull String $receiver) {
  System.out.println($receiver);
}
fun Random.randomStrength(): Int {
  return nextInt(100) + 10
}

fun Random.randomDamage(strength: Int): Int {
  return (strength * 0.1 + nextInt(10)).toInt()
}

fun Random.randomBlock(): Boolean {
  return nextBoolean()
}
private var random: Random = Random()

init {
  strength = random.randomStrength()
  report("Created (strength $strength)")
}

fun damage(damage: Int) {
  val blocked = random.randomBlock()

  if (blocked) {
    report("Blocked attack")
    return
  }

  health -= damage
  report("Damage -$damage, health $health")

  if (health <= 0) {
    isAlive = false
  }
}

fun attack(robot: Robot) {
  val damage = random.randomDamage(strength)
  robot.damage(damage)
}

Lambdas with receivers

Just as you can specify a receiver for an extension function, you can do so for a lambda as well.

fun beginBattle(firstRobot: Robot, secondRobot: Robot,
                onBattleEnded: Robot.() -> Unit) {
  var winner: Robot? = null
  battle(firstRobot, secondRobot)
  winner = if (firstRobot.isAlive) firstRobot else secondRobot
  winner.onBattleEnded()
}
onBattleEnded(winner)

Anonymous functions

Anonymous functions are more or less the same as ordinary ones, but they don’t have a name. To invoke them, you need to assign them to a variable or pass them as an argument to another function. Consider the following snippet of code:

fun(robot: Robot) {
  robot.report("Won!")
}
val reportOnWin = fun(robot: Robot) { robot.report("Won!") }
Battlefield.beginBattle(firstRobot, secondRobot, reportOnWin)
fun beginBattle(firstRobot: Robot, secondRobot: Robot, onBattleEnded: (Robot) -> Unit)
Battlefield.beginBattle(firstRobot, secondRobot, fun(robot) {
  robot.report("Won!")
})

Returning from lambdas

If you use a regular return expression inside a lambda, you’ll return to the call site of the outer function. That is, the return in the lambda also returns from the outer function.

fun calculateEven() {
  var result = 0

  (0..20).forEach {
    if (it % 3 == 0) return
    
    if (it % 2 == 0) result += it
  }

  println(result)
}
fun calculateEven() {
  var result = 0

  (0..20).forEach {
    if (it % 3 == 0) return@forEach
  
    if (it % 2 == 0) result += it
  }

  println(result)
}
fun calculateEven() {
  var result = 0

  (0..20).forEach loop@{
    if (it % 3 == 0) return@loop

    if (it % 2 == 0) result += it
  }

  println(result)
}
fun calculateEven() {
  var result = 0

  (0..20).forEach(fun(value) {
    if (value % 3 == 0) return

    if (value % 2 == 0) result += value
  })

  println(result)
}

Inline functions

Remember that, for each lambda that you create to pass to another function, the Kotlin compiler generates an appropriate class extending FunctionN. In some cases, that might not be a good solution, especially when you do it multiple times, as this will increase memory usage and have a performance impact on your application.

inline fun beginBattle(firstRobot: Robot, secondRobot: Robot,
            onBattleEnded: Robot.() -> Unit) {
  ...
}
public static final void main(@NotNull String[] args) {
  Robot firstRobot = new Robot("Experimental Space Navigation Droid");
  Robot secondRobot = new Robot("Extra-Terrestrial Air Safety Droid");
  Battlefield this_$iv = Battlefield.INSTANCE;
  Robot winner$iv = (Robot)null;
  this_$iv.battle(firstRobot, secondRobot);
  winner$iv = firstRobot.isAlive() ? firstRobot : secondRobot;
  winner$iv.report("Win!");
}

@Suppress("NOTHING_TO_INLINE")
inline fun someFunction() {
  ...
}

noinline

If you don’t want some of the lambda parameters to be inlined along with the higher-order function, you can mark the lambda as noinline. A FunctionN instance will still be generated for noinline lambda:

inline fun someFunction(inlinedLambda: () -> Unit,
        noinline nonInlinedLambda: () -> Unit) {
  ...
}

crossinline

The crossinline keyword is used to mark a lambda parameter which shouldn’t allow a non-local return (i.e., return without a label). This is useful when a function, which receives a lambda, will call it inside another lambda. In this case, it’s not allowed to return from such a lambda. Take a look at the example below:

inline fun someFunction(body: () -> Unit) {
  yetAnotherFunction {
    body()
  }
}

inline fun someFunction(crossinline body: () -> Unit) {
  yetAnotherFunction {
    body()
  }
}
fun oneMoreFunction() {
  someFunction {
    return
  }
}

Tail recursive functions

The last expression in a function is called the tail call. If, in some cases, the function gets called again in the tail call expression, this function is called tail-recursive. In Kotlin, you can mark such functions as tailrec, and the Kotlin complier will replace the recursion by an appropriate loop for the sake of performance optimization. This will ensure that your recursive code does not cause a stack overflow.

tailrec fun battle(firstRobot: Robot, secondRobot: Robot) {
  ...
}
public final void battle(@NotNull Robot firstRobot, @NotNull Robot secondRobot) {
  do {
    firstRobot.attack(secondRobot);
    if (!secondRobot.isAlive()) {
      return;
    }

    secondRobot.attack(firstRobot);
  } while (firstRobot.isAlive());
}

Collections standard library

The use of standard library functions on collections that you saw in Chapter 10 are further examples of functional programming. The Kotlin standard library offers you a huge amount of useful functions for collection processing.

val participants = arrayListOf<Robot>(
  Robot("Extra-Terrestrial Neutralization Bot"),
  Robot("Generic Evasion Droid"),
  Robot("Self-Reliant War Management Device"),
  Robot("Advanced Nullification Android"),
  Robot("Rational Network Defense Droid"),
  Robot("Motorized Shepherd Cyborg"),
  Robot("Reactive Algorithm Entity"),
  Robot("Ultimate Safety Guard Golem"),
  Robot("Nuclear Processor Machine"),
  Robot("Preliminary Space Navigation Machine")
)
val topCategory = participants.filter { it.strength > 80 }
val topCategory = participants
  // 1
  .filter { it.strength > 80 }
  // 2
  .take(3)
  // 3
  .sortedBy { it.name }

Infix notation

If a function is a member function or an extension function and receives only one argument, you can mark it with the infix keyword. That way you can invoke it without a dot and parentheses.

infix fun attack(robot: Robot) {
  val damage = random.randomDamage(strength)
  robot.damage(damage)
}
firstRobot attack secondRobot

Sequences

In Kotlin, you can use Sequence to create a lazily evaluated collection so that you can operate on collections of unknown size, which can be potentially infinite.

val random = Random()
// 1
val sequence = generateSequence {
  // 2
  random.nextInt(100)
}

sequence
  // 3
  .take(15)
  // 4
  .sorted()
  // 5
  .forEach { println(it) }

val factorial = generateSequence(1 to 1) {
  it.first + 1 to it.second * (it.first + 1)
}

println(factorial.take(10).map { it.second }.last())

Challenges

Key points

Where to go from here?

Functional programming opens a world of possibilities, which are difficult to cover entirely in a single chapter. But to deepen your knowledge after understanding the basics, you can move on to more advanced concepts, such as function composition, Either, Option, Try, and more.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now