Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Functional Programming

Functional Programming is a programming paradigm (a way of programming) where code is written in a declarative way, leveraging features like composition of functions & functions treated as first-class citizens,.

Features

All three features might seem like alien concepts. We’ll understand each of these concepts with appropriate examples in later sections.

Kotlin fully supports Functional programming. In Kotlin, functions are first-class citizens i.e. functions can be saved as variables, passed as function arguments & can also be returned from a function. For simplicity, we will call such functions as Lambda functions.

To enable Functional Programming, Kotlin provides features like Function types, Lambda expressions and Higher-order functions. Let us try to understand each of these features.

Lambda function

Lambda function is a function stored in a variable, passed as a function argument or returned from a function.

Definition

Recall that we use fun keyword to define functions in Kotlin, such functions are called Named functions. We can define lambda functions & store them in a variable using the following syntax :

 val funName: /* Function Type */ = /* Lambda Expression */
// OR
val funName: (/* Inputs */) -> /* Output */ = { /* Inputs */ ->
		// Code
		/* Output value */
}
 

Example :

 fun double(x: Int) = x * 2
// can be written as
val double: (Int) -> Int = { it * 2 }
 

Lambda functions are declared using Function type notation & defined using Lambda Expressions. Let us now understand the syntax of both.

Function type () → X

Function type is a notation used to define the type of a Lambda function.

 (/* Inputs */) -> /* Output */

// Examples : 
() -> Unit                // No input, no output
(Int) -> String           // Int input, String output
(String) -> Unit          // String input, no output
(Int, String) -> String   // (Int, String), String output
 

Lambda Expressions {}

Lambda Expressions are function literals i.e. they are used to define the body of a lambda function against a given function type (without named function declaration).

 { /* Input params */ ->
		// Code
		/* Output value */
}

// Examples :
{ } // No input params or single input param referenced as "it"
{ x -> } // Single input param referenced as x
{ x: Int, y: String -> } // Two input params with type explicitly defined
 

Note :

  • Single input parameter can be referenced using it identifier
  • We don’t use return keyword to return a value in lambda expression. Implicitly, the value represented by the last expression in the lambda expression is returned.
  • Lambda expressions express a value of type same as the return type of function type. For example, for a function type Int → String, its lambda expression will express a value of type String.

Invocation

Lambda functions can be invoked exactly the same way we invoke normal functions.

 /* funName */(/* Input args */)

// Example :
double(512)
decorPrint("The Himalayas")
 

Lambda functions can also be invoked by calling its invoke function :

 /* funName */.invoke(/* Input args */)

// Example :
double.invoke(512)
decorPrint.invoke("The Himalayas")
 

Examples

Example 1 (Single input, no output)

Let’s define a lambda function decorPrint of type (String) → Unit which prints the input text in the format -*-*-> {text-here} <-*-*-.

Following are the various ways in which we can do so :

Type explicitly defined & input referenced using it identifier

 val decorPrint: (String) -> Unit = {
		print("-*-*-> $it <-*-*-")
}
 

Note that input parameter is referenced using it identifier.

Type explicitly defined & input referenced using custom identifier

 val decorPrint: (String) -> Unit = { text ->
    print("-*-*-> $text <-*-*-")
}
 

Note that type of text is implicitly known from the function type (String) -> Unit

Type implicitly defined

 val decorPrint = { text: String ->
    print("-*-*-> $text <-*-*-")
}
 

Note :

  • Input type is explicitly defined as String
  • Output type is inferred from the return type of print function i.e. Unit.
  • Function type is implicit & inferred from the Lambda expression i.e. (String) → Unit

We can call the function as follows :

 fun main() {
		val decorPrint = { text: String ->
		    print("-*-*-> $text <-*-*-")
		}

		decorPrint("The Himalayas")
}

// Output : -*-*-> The Himalayas <-*-*-
 

Example 2 (Single input, single output)

Let’s define a lambda function roundToNearestTens of type (Int) → Int which rounds off the input number to nearest tens place. Examples - 123 → 120, 435 → 440.

Following are the various ways in which we can do so :

Type explicitly defined & input referenced using it identifier

 val roundToNearestTens: (Int) -> Int = {
    val onesPlace = it % 10
    when {
        onesPlace < 5 -> it - onesPlace
        else -> it + 10 - onesPlace
    }
}
 

Note that input parameter is referenced using it identifier.

Type explicitly defined & input referenced using custom identifier

 val roundToNearestTens: (Int) -> Int = { x ->
    val onesPlace = x % 10
    when {
        onesPlace < 5 -> x - onesPlace
        else -> x + 10 - onesPlace
    }
}
 

Note that type of x is implicitly known from the function type (Int) -> Int

Type implicitly defined

 val roundToNearestTens = { x: Int ->
    val onesPlace = x % 10
    when {
        onesPlace < 5 -> x - onesPlace
        else -> x + 10 - onesPlace
    }
}
 

Note

  • Input type is explicitly defined as Int
  • Output type is inferred from the when expression i.e. Int.
  • Function type is implicit & inferred from the Lambda expression i.e. (Int) → Int

We can call the function as follows :

 fun main() {
		val roundToNearestTens = { x: Int ->
		    val onesPlace = x % 10
		    when {
		        onesPlace < 5 -> x - onesPlace
		        else -> x + 10 - onesPlace
		    }
		}

		println(roundToNearestTens(123)) // 120
		println(roundToNearestTens(437)) // 440
}
 

Example 3 (Multiple inputs, single output)

Let’s define a lambda function multiplyString of type (text: String, noOfTimes: Int) → String which returns a string concatenated with itself. Examples - ha * 3 = hahaha.

Following are the various ways in which we can do so :

Type explicitly defined & inputs referenced using custom identifier

 val multiplyText: (String, Int) -> String = { text, noOfTimes ->
    var result = ""
    repeat(noOfTimes) {
        result += text
    }
    result
}
 

Note that type of text & noOfTimes is implicitly known from the function type (String, Int) -> String

Type implicitly defined

 val multiplyText = { text: String, noOfTimes: Int ->
    var result = ""
    repeat(noOfTimes) {
        result += text
    }
    result
}
 

Note

  • Input type is explicitly defined as (String, Int)
  • Output type is inferred from the last expression i.e. result i.e. Int
  • Function type is implicit & inferred from the Lambda expression i.e. (String, Int) → Int
  • return keyword is not used

We can call the function as follows :

 fun main() {
		val multiplyText = { text: String, noOfTimes: Int ->
		    var result = ""
		    repeat(noOfTimes) {
		        result += text
		    }
		    result
		}

		println(multiplyText("ha", 3)) // hahaha
}
 

Higher order functions

Higher order functions are functions that either take function as an argument or return a function as output. You might wonder, why do we want to do that.

Need

Higher order functions have two major applications :

  • They are used in Functional Programming as it often requires passing functions as an argument to another function.
  • They are used as callback functions when dealing with asynchronous operations

Function as an argument

To pass function as an argument, we use the Function type notation to define the type of argument :

 fun /* fun name */(
		/* Argument fun name */: /* Function type */
): /* return type */

// Example :
fun doMultipleTimes(
		noOfTimes: Int,
		what: () -> Unit, // Lambda function of type () -> Unit
)
 

Example1 (GenerateSeries)

Write a function - generateSeries to print a series of numbers based on the passed arguments :

  • firstTerm - first term of the series
  • getNextTerm - function that gets a term as input & returns its next term
  • noOfTerms - number of terms to print

Example :

  • Inputs : firstTerm = 5, getNextTerm = 2*x, noOfTerms = 5
  • Output : 5, 10, 20, 40, 80
 fun generateSeries(
    name: String,
    firstTerm: Int,
    noOfTerms: Int = 10,
    getNextTerm: (Int) -> Int
) {
    print("$name -> $firstTerm, ")

    var currentTerm = firstTerm
    repeat(noOfTerms - 1) {
        currentTerm = getNextTerm(currentTerm)
        print("$currentTerm, ")
    }
    println("\b\b")
}

fun main() {
    // Function Call with named arguments
    generateSeries(
        name = "1. x + 1",
        firstTerm = 1,
        getNextTerm = { it + 1 }
    )

    // Function Call with custom lambda argument name "x"
    generateSeries(
        name = "2. x * 2",
        firstTerm = -5,
        getNextTerm = { x -> x * 2 }
    )

    // Function Call with trailing lambda expression
    generateSeries(
        name = "3. 2 * x + 5",
        firstTerm = 10
    ) { x -> 2 * x + 5 }

    generateSeries("4. x / 2", 1024) { it / 2 }
}

/* Output :

1. x + 1 -> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
2. x * 2 -> -5, -10, -20, -40, -80, -160, -320, -640, -1280, -2560
3. 2 * x + 5 -> 10, 25, 55, 115, 235, 475, 955, 1915, 3835, 7675
4. x / 2 -> 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2
 */
 

Trailing Lambda expression

When calling a function whose last argument is a Function Type / Lambda function, we need not write its corresponding lambda expression inside the parenthesis (). It can be written outside the outside the parenthesis. When written outside (), the Lambda expression is known as Trailing Lambda expression.

 /* funName */(..., /* Lambda expression */) // Normal call
/* funName */(...) /* Lambda expression */ // Call with Trailing lambda expression

// Example :
// Function where last argument is a Lambda function
fun generateSeries(
    name: String,
    firstTerm: Int,
    noOfTerms: Int = 10,
    getNextTerm: (Int) -> Int
)

// Normal call
generateSeries(
		name = "2. x * 2",
		firstTerm = -5,
		getNextTerm = { x -> x * 2 }
)

// Call with Trailing lambda expression
generateSeries(
		name = "3. 2 * x + 5",
		firstTerm = 10
) { x -> 2 * x + 5 }
 

Pass named function as a Lambda function

When invoking a Higher order function that receives a Lambda function as a paramter, in place of Lambda function we can also pass a named function instead of Lambda expression. To do so, we use the Referencing operator :: :

 /* funName */({ /* Lambda Expression */}) // Lambda expression passed
/* funName */(::/* another funName */) // Named function passed

// Example :
// Function call with Lambda expression
generateSeries(
		name = "2. x * 2",
		firstTerm = -5,
		getNextTerm = { x -> x * 2 }
)

// Named function getNextTerm
fun getNextTerm(x: Int) = x*2

// Function call with Named function
generateSeries(
		name = "2. x * 2",
		firstTerm = -5,
		getNextTerm = ::getNextTerm
)
 

Note

  • type of named function should match the argument’s function type required. Here both - named function & argument’s function type are (Int) → Int, so it worked.
  • parenthesis () are not used when passing the function

We can pass any class’s member function also in place of a Lambda function, using member reference :

 /* Class name */::/* funName */

// Example :
String::uppercase
 

Function as a return type

To return a function from another function, we use Function Type as the return type of that function :

 fun /* funName */(): /* Function Type */    // Returns a function

// Example :
fun getFun(): () -> Unit    // Returns a function of type () -> Unit
 

Imperative vs Functional

In Declarative programming, we write what needs to be done. Whereas in Imperative programming, we write how to do it. Let us understand the difference between the with the help of an example :

Example (PhotoRenaming)

Given a list of photo info, return list of new names for each photo based on the following criteria.

  • Photo info is in String format - name, city, time.
  • New name of photo should be in format <city><index>.<extension>. Here index is index of photo when all photos of that particular city are sorted by time. Indexing starts from 1.
  • New names for each photo should be returned in the same order as that of input.

Example

Input

 [ FS.jpg, Udaipur, 2019-09-05 14:08:15,
IG.png, Delhi, 2021-06-20 15:13:22,
SKB.png, Udaipur, 2019-09-05 14:07:13,
TH.jpg, Mumbai, 2021-07-23 08:03:02,
GOI.jpg, Mumbai, 2021-07-22 23:59:59 ]
 

Output

 [ Udaipur2.jpg,
Delhi1.png,
Udaipur1.png,
Mumbai2.jpg,
Mumbai1.jpg ]
 

Explanation

  • Photo # 1 & 3 are of Udaipur where #3 is taken before #1. So #1 is renamed as Udaipur2 & #3 is renamed as Udaipur1.
  • Photo # 2 is the only photo of Delhi so it is renamed as Delhi1.
  • Photo # 4 & 5 are of Mumbai where #5 is taken before #4. So #4 is renamed as Mumbai2 & #5 is renamed as Mumbai1.

Algorithm

  • Step 1 : Create a class Photo to save Photo info
  • Step 2 : Create List<Photo> from photoInfoList
  • Step 3 : Group photos based on City i.e. create cityToPhotosMap : Map<City, List<Photo>> from List<Photo>
  • Step 4 : For each city in cityToPhotosMap, sort List<Photo> by time
  • Step 5 : Prepare Map<oldName, newName>
  • Step 6 : Arrange the newNames based on the input order & return newNames

Imperative approach

 private fun getNewNames(photoInfoList: List<String>): List<String> {

    // Step 1 : Create a class to save Photo info
    class Photo(
        val name: String,
        val city: String,
        val time: String
    ) {
        // Returns the photo info in original format
        override fun toString() = "$name, $city, $time"

        // Returns the extension of photo
        fun extension() = name.substringAfterLast(".")
    }

    // Step 2 : Create List<Photo> from names
    val photos = mutableListOf<Photo>()
    photoInfoList.forEach { photo ->
        val fields = photo.split(", ")
        photos.add(
            Photo(
                name = fields[0],
                city = fields[1],
                time = fields[2]
            )
        )
    }

    // Step 3 : Create Map<City, List<Photo>> from List<Photo>
    val cityToPhotosMap = mutableMapOf<String, List<Photo>>()
    photos.forEach { photo ->
        val list = cityToPhotosMap.getOrElse(photo.city) { emptyList() }
        cityToPhotosMap[photo.city] = list + photo
    }

    // Step 4 : For each city in cityToPhotosMap, sort List<Photo> by time
    cityToPhotosMap.forEach { (city, cityPhotos) ->
        val sortedList = cityPhotos.sortedBy {
            SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(it.time).time
        }
        cityToPhotosMap[city] = sortedList
    }

    // Step 5 : Prepare Map<oldName, newName>
    val oldToNewNamesMap = mutableMapOf<String, String>()
    cityToPhotosMap.forEach { (city, cityPhotos) ->
        cityPhotos.forEachIndexed { index, photo ->
            val newName = "$city${index + 1}.${photo.extension()}"
            oldToNewNamesMap[photo.toString()] = newName
        }
    }

    // Step 6 : Arrange the newNames based on input order
    val namesToReturn = mutableListOf<String>()
    photoInfoList.forEach {
        val newName = oldToNewNamesMap[it] ?: error("New name not found!")
        namesToReturn.add(newName)
    }

    // Return newNames
    return namesToReturn
}

fun main() {
		println(
				getNewNames(
						listOf(
								"FS.jpg, Udaipur, 2019-09-05 14:08:15",
								"IG.png, Delhi, 2021-06-20 15:13:22",
								"SKB.png, Udaipur, 2019-09-05 14:07:13",
								"TH.jpg, Mumbai, 2021-07-23 08:03:02",
								"GOI.jpg, Mumbai, 2021-07-22 23:59:59"
						)
				)
		)
}
 

Note :

  • Manual looping in each step shows that the focus is on How to do and not What to do

Declarative approach

 // Step 1 : Create a class to save Photo info
private class Photo(
    val name: String,
    val city: String,
    val time: String
) {
    companion object {
        fun createFromFormat(format: String): Photo {
            val fields = format.split(", ")
            return Photo(
                name = fields[0],
                city = fields[1],
                time = fields[2]
            )
        }
    }

    // Returns the photo info in original format
    override fun toString() = "$name, $city, $time"

    // Returns the extension of photo
    fun extension() = name.substringAfterLast(".")
}

private fun getNewNames(photoInfoList: List<String>): List<String> {

    val oldToNewNamesMap = photoInfoList

        // Step 2 : Create List<Photo> from names
        .map { Photo.createFromFormat(it) }

        // Step 3 : Create Map<City, List<Photo>> from List<Photo>
        .groupBy { it.city }

        // Step 4 : For each city in cityToPhotosMap, sort List<Photo> by time
        .mapValues { (_, cityPhotos) -> cityPhotos.sortedByTime() }

        // Step 5 : Prepare Map<oldName, newName>
        .map { (city, cityPhotos) -> cityPhotos.mapToNewNames(city) }.flatten().toMap()

    // Step 6 : Arrange the newNames based on input order & return
    return photoInfoList.map {
        oldToNewNamesMap[it] ?: error("New name not found!")
    }
}

// Returns new list sorted by time
private fun List<Photo>.sortedByTime(): List<Photo> {
    return sortedBy {
        SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(it.time).time
    }
}

// Returns old to new name pairs list
private fun List<Photo>.mapToNewNames(city: String): List<Pair<String, String>> {
    return mapIndexed { index, photo ->
        val newName = "$city${index + 1}.${photo.extension()}"
        photo.toString() to newName
    }
}

fun main() {
		println(
				getNewNames(
						listOf(
								"FS.jpg, Udaipur, 2019-09-05 14:08:15",
								"IG.png, Delhi, 2021-06-20 15:13:22",
								"SKB.png, Udaipur, 2019-09-05 14:07:13",
								"TH.jpg, Mumbai, 2021-07-23 08:03:02",
								"GOI.jpg, Mumbai, 2021-07-22 23:59:59"
						)
				)
		)
}
 

Note :

  • The use of Kotlin STL functions like map, groupBy, mapValues, user defined function like Photo.createFromFormat and extension functions like List<Photo>.sortedByTime, List<Photo>.mapToNewNames make the program declarative where focus is on What to do rather than How to do.
  • The above program leverages composition of functions.
    • photoInfoList: List<String>mapList<Photo>groupByMap<String, List<Photo>>mapValuesMap<String, List<Photo>>mapList<List<Pair<String, String>>>flattenList<Pair<String, String>>toMapMap<String, String>
  • Kotlin STL functions like map, groupBy, mapValues are Higher order functions that take lambda function as input parameter. Hence, it is an example of Functional programming.

In Functional Programming / Declarative approach, a large program is often broken down into several smaller functions. Hence, making the program easier to debug & maintain. Also, tweaks & addition of new funtionalities is also easier as compared to Imperative approach.

Following is a comparison between the two paradigms :

Imperative Declarative / Functional
Focus How to do What to do
Building blocks Loops STL functions like map, filter, associate, groupBy, reduce etc.
Code Readability Poor Better
Maintenance Tough Easy
Easeness to debug Poor Better