Contents

Functions

Default and Named Arguments

In most programming languages, when calling a function, we are required to specify all the arguments that the function accepts. However, in Kotlin, this constraint is lifted, making function calls more flexible. Kotlin allows us to omit certain arguments when calling a function by providing default values for parameters. This is one of the powerful features of Kotlin, which simplifies function calls and enhances readability.

In Kotlin, function parameters are defined using Pascal notation (i.e., name: data_type) and separated by commas. By assigning default values to parameters, we can make them optional, allowing the function to use the provided defaults if arguments are not passed during the function call.

There are two types of arguments in Kotlin –  

1. Default arguments
2. Named arguments

Kotlin Default arguments 

The arguments which need not specify explicitly while calling a function are called default arguments. 
If the function is called without passing arguments then the default arguments are used as function parameters. In other cases, if arguments are passed during a function call then passed arguments are used as function parameters.

There are three cases for default arguments-  

1. No arguments are passed while calling a function
2. Partial arguments are passed while calling a function
3. All arguments are passed while calling a function

1. No arguments are passed while calling a function 

When no argument is passed while calling a function then the default arguments are used as function parameters. We need to initialize the variables while defining a function. 

Kotlin program of calling student() function without passing an arguments

				
					// Function with default parameter values
fun functionName(param1: Type = defaultValue1, param2: Type = defaultValue2) {
    // Function body
}

// Function call examples
functionName()                        // Uses all default values
functionName(param1 = value1)          // Overrides param1, uses default for param2
functionName(param2 = value2)          // Overrides param2, uses default for param1
functionName(param1 = value1, param2 = value2)  // Overrides both param1 and param2
functionName(param2 = value2, param1 = value1)  // Named arguments allow reordering

				
			

Example:

				
					// Function with default arguments for employee details
fun employeeDetails(name: String = "John", department: String = "HR", id: Int = 101) {
    println("Employee Name: $name")
    println("Department: $department")
    println("Employee ID: $id")
}

fun main() {
    // Calling the function with no arguments (uses all default values)
    employeeDetails()

    // Calling the function with one argument (overrides the default name)
    employeeDetails(name = "Alice")

    // Calling the function with two arguments (overrides name and department)
    employeeDetails(name = "Bob", department = "IT")

    // Calling the function with all arguments (overrides all default values)
    employeeDetails(name = "Charlie", department = "Finance", id = 105)
}

				
			

Output:

				
					Hello, Guest!
Hello, Alice!
Hi, Bob!
Welcome, Charlie!

				
			

2. Partial arguments are passed while calling a function –

Here some of the arguments are passed while calling a function and these are used as function parameters. If any formal parameter does not get value from the function call then the default value will be used for that parameter.

Kotlin program of calling student() function with passing some arguments.

				
					// Default arguments in function definition: name, department, and salary
fun employeeDetails(name: String = "John Doe", department: String = "HR", salary: Int = 50000) {
    println("Employee Name: $name")
    println("Department: $department")
    println("Salary: $salary")
}

fun main(args: Array<String>) {
    val employee_name = "Alice"
    val employee_department = "Finance"
    
    // Passing only two arguments: name and department
    employeeDetails(employee_name, employee_department)
}

				
			

Output:

				
					Employee Name: Alice
Department: Finance
Salary: 50000
				
			

3. All arguments are passed while calling a function –

Here, we have to pass all the arguments as defined in the function definition but data type of actual arguments must match with data type of formal arguments in the same order. 

Kotlin program of calling student() function with passing all the arguments

				
					// Default arguments in function definition: name, category, and price
fun productDetails(name: String = "Generic Product", category: String = "General", price: Double = 100.0) {
    println("Product Name: $name")
    println("Category: $category")
    println("Price: $$price")
}

fun main(args: Array<String>) {
    val product_name = "Laptop"
    val product_category = "Electronics"
    val product_price = 1500.0

    // Passing all the arguments of product name, category, and price in the same order as defined
    productDetails(product_name, product_category, product_price)
}

				
			

Output:

				
					Product Name: Laptop
Category: Electronics
Price: $1500.0

				
			

Recursion

Like other programming languages, we can use recursion in Kotlin. A function that calls itself is called a recursive function and this process of repetition is called recursion. Whenever a function is called then there are two possibilities:

1. Normal function call
2. Recursive function call

1. Normal function callWhen a function is called from main() block then it is called a normal function call. In below example, sum() is called at a time and it executes its instruction and terminate with returning the sum of number. If we want to execute the function again then we should call sum() from the main block one more time. 

Calling sum() function from main() block

2. Recursive Function Call : When a function invokes itself, it is known as a recursive function call. Every recursive function must have a termination condition, otherwise, the program may enter an infinite loop, potentially causing a stack overflow error.

For example, calling the callMe() function from within its own body represents a recursive function call.

Example 1:- Recursive function to calculate the sum of first N natural numbers

				
					// Recursive function to calculate the sum of first N natural numbers
fun sumOfNumbers(n: Int): Int {
    // Base case: if n is 0, return 0
    return if (n == 0) {
        0
    } else {
        // Recursive case: add n to the result of sumOfNumbers(n - 1)
        n + sumOfNumbers(n - 1)
    }
}

fun main() {
    val number = 5
    val result = sumOfNumbers(number)
    println("The sum of first $number natural numbers is: $result")
}

				
			

Output:

				
					The sum of first 5 natural numbers is: 15

				
			

Example 2:- Recursive Function to Calculate GCD

				
					// Recursive function to calculate the GCD of two numbers
fun gcd(a: Int, b: Int): Int {
    return if (b == 0) {
        a  // Base case: when b becomes 0, return a
    } else {
        gcd(b, a % b)  // Recursive case: call gcd with b and a % b
    }
}

fun main() {
    val num1 = 48
    val num2 = 18
    val result = gcd(num1, num2)
    println("The GCD of $num1 and $num2 is: $result")
}

				
			

Output:

				
					The GCD of 48 and 18 is: 6

				
			
Advantages of Recursion in Kotlin:

1. Simplifies Complex Problems: Breaks problems into smaller sub-problems, making them easier to solve (e.g., tree traversal, factorial).
2. Concise Code: Recursive functions are often shorter and more readable than iterative solutions.
3. Modularity: Each recursive call solves a part of the problem, leading to a clear, step-by-step approach.
4. Expressiveness: Ideal for algorithms like divide-and-conquer and backtracking.
5. Tail Recursion Optimization: Kotlin optimizes tail-recursive functions, reducing the risk of stack overflow.

Disadvantages of Recursion in Kotlin:

1. Stack Overflow Risk: Deep recursion can cause stack overflow errors.
2. Higher Memory Usage: Recursive calls add more stack frames, using more memory.
3. Slower Performance: Recursive function calls can be slower due to call stack management.
4. Complex Debugging: Tracking multiple recursive calls can make debugging harder.
5. Limited by Call Stack Size: Deep recursion is limited by system stack depth.

Tail Recursion

In traditional recursion, the recursive call is made first, and the results are processed after the call returns. In tail recursion, the computation is done first, and the recursive call is made afterward, passing the result to the next call. Both methods produce the same result, but the key difference is how they manage their memory and function calls.

In tail recursion, the recursive call must be the final action performed by the function. This allows the compiler to optimize the recursion by reusing the current function’s stack frame, thereby avoiding excessive memory usage and preventing StackOverflowError.

Benefits of Tail Recursion:

  • Memory Optimization: Since there’s no need to store the current function call in the stack, the stack space is reused for the next recursive call.
  • Prevention of StackOverflowError: Tail-recursive functions avoid deep call stacks and do not run into stack overflow issues.

 

Example 1: Finding the Factorial Using Tail Recursion

				
					// Tail-recursive function to calculate factorial
tailrec fun factorial(num: Int, accumulator: Long = 1): Long {
    return if (num == 1)  // Base case
        accumulator
    else
        factorial(num - 1, accumulator * num)  // Tail recursion
}

fun main() {
    val number = 5
    val result = factorial(number)
    println("Factorial of $number is: $result")
}

				
			

Flowchart:

				
					Factorial of 5 is: 120
				
			

Example 2: Finding the Sum of Array Elements Using Tail Recursion

				
					// Tail-recursive function to find sum of array elements
tailrec fun sum(array: Array<Int>, index: Int, currentSum: Int = 0): Int {
    return if (index <= 0)  // Base case: index reaches 0
        currentSum
    else
        sum(array, index - 1, currentSum + array[index - 1])  // Tail recursion
}

fun main() {
    val array = arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    val totalSum = sum(array, array.size)
    println("The sum of array elements is: $totalSum")
}

				
			

Output:

				
					The sum of array elements is: 55

				
			

Expressions and Anonymous Functions

Lambdas are anonymous functions that can be treated as values, passed as arguments, or returned from functions. They are defined using curly braces {}.

Syntax:

				
					val lambda_name : Data_type = { argument_List -> code_body }

				
			

A lambda expression in Kotlin is surrounded by curly braces {}. Inside the curly braces, the argument declarations go first, followed by the code body after the -> arrow. If the return type of the lambda is not Unit, the last expression in the lambda body is automatically considered as the return value.

Example:

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

				
			

Simplified Lambda Expression:

Note: A lambda expression doesn’t always require a variable. It can be passed directly as an argument to a function.

Kotlin Program Using Lambda Expressions:

				
					// Lambda with type annotations
val sum1 = { a: Int, b: Int -> a + b }

// Lambda without explicit type annotations
val sum2: (Int, Int) -> Int = { a, b -> a + b }

fun main(args: Array<String>) {
    val result1 = sum1(2, 3)
    val result2 = sum2(3, 4)

    println("The sum of two numbers is: $result1")
    println("The sum of two numbers is: $result2")

    // Directly printing the result of a lambda expression
    println(sum1(5, 7))
}

				
			

Output:

				
					The sum of two numbers is: 5
The sum of two numbers is: 7
12
				
			
Type Inference in Lambdas

Kotlin’s type inference allows the compiler to determine the type of a lambda expression. For example, in the following lambda expression, the compiler infers the types of parameters based on context:

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

The compiler treats this lambda as a function of type (Int, Int) -> Int.

If you want to return a string instead, you can use Kotlin’s built-in toString() function.

Example:

				
					val sum1 = { a: Int, b: Int ->
    val num = a + b
    num.toString()  // Converts the result to String
}

fun main() {
    val result = sum1(2, 3)
    println("The sum of two numbers is: $result")
}

				
			

Output:

				
					The sum of two numbers is: 5

				
			

Lambda Type Declaration and Examples:

We can declare the types of input and output explicitly in lambdas. The pattern is (InputType) -> OutputType.

Syntax:

				
					val lambda1: (Int) -> Int = { a -> a * a }
val lambda2: (String, String) -> String = { a, b -> a + b }
val lambda3: (Int) -> Unit = { println(it) }  // No return value

				
			

Example:

				
					val lambda4: String.(Int) -> String = { this + it }

fun main() {
    val result = "Hello".lambda4(42)
    println(result)
}

				
			

Output:

				
					Hello42

				
			
Implicit Parameter (it)

When a lambda has a single parameter, Kotlin provides an implicit parameter called it, which can be used to reference that parameter.

Shorthand form using it:

				
					val numbers = arrayOf(1, -2, 3, -4, 5)

fun main() {
    println(numbers.filter { it > 0 })  // Uses implicit parameter 'it'
}

				
			

Output:

				
					[1, 3, 5]

				
			

Longhand form:

				
					val numbers = arrayOf(1, -2, 3, -4, 5)

fun main() {
    println(numbers.filter { item -> item > 0 })  // Explicit parameter
}

				
			

Output:

				
					[1, 3, 5]
				
			
Returning Values from Lambda Expressions

A lambda expression returns the final value of the last expression. The return value can be of any type, such as an Int, String, or Boolean.

Kotlin Program Returning String from Lambda:

				
					val find = fun(num: Int): String {
    return when {
        num % 2 == 0 && num < 0 -> "Number is even and negative"
        num % 2 == 0 && num > 0 -> "Number is even and positive"
        num % 2 != 0 && num < 0 -> "Number is odd and negative"
        else -> "Number is odd and positive"
    }
}

fun main() {
    val result = find(112)
    println(result)
}

				
			

Output:

				
					Number is even and positive
				
			
Anonymous Functions

An anonymous function is like a regular function, except it doesn’t have a name. It can be used as an expression or a block.

Example 1: Anonymous Function as an Expression:

				
					val multiply = fun(a: Int, b: Int): Int = a * b

				
			
				
					val anonymous1 = fun(x: Int, y: Int): Int = x + y
val anonymous2 = fun(a: Int, b: Int): Int {
    val mul = a * b
    return mul
}

fun main() {
    val sum = anonymous1(3, 5)
    val mul = anonymous2(3, 5)
    println("The sum of two numbers is: $sum")
    println("The multiply of two numbers is: $mul")
}

				
			

Output:

				
					The sum of two numbers is: 8
The multiply of two numbers is: 15

				
			

Inline Functions

In Kotlin, higher-order functions and lambda expressions are stored as objects, which can introduce memory overhead due to memory allocation for function objects and virtual calls. This overhead can be minimized by using the inline keyword. By marking a function as inline, we instruct the compiler to avoid allocating memory for the function and to copy the function’s code directly to the call site, thus improving performance.

Example:

				
					fun higherfunc(str: String, mycall: (String) -> Unit) {
    mycall(str)  // Calls print() by passing str
}

// Main function
fun main(args: Array<String>) {
    print("HelloKotlin: ")
    higherfunc("A Computer Science portal for Kotlin", ::print)
}

				
			
Bytecode Explanation:

Like Java, Kotlin is platform-independent, and code is compiled to bytecode. To view bytecode, go to Tools -> Kotlin -> Show Kotlin Bytecode and decompile it.

In the bytecode, the main thing to notice is:

				
					mycall.invoke(str)
				
			

Here, mycall invokes the print function by passing str as a parameter, which adds an extra function call and memory overhead. If multiple such function objects are created, it can lead to increased method counts and a higher memory footprint.

How Inline Helps:

				
					inline fun higherfunc(str: String, mycall: (String) -> Unit) {
    mycall(str)  // Calls print() by passing str
}

// Main function
fun main(args: Array<String>) {
    print("GeeksforGeeks: ")
    higherfunc("A Computer Science portal for Geeks", ::print)
}

				
			
Non-Local Control Flow with Inline Functions

In Kotlin, normally, you cannot return from a lambda expression directly. However, using the inline keyword allows you to return from the lambda and also exit the calling function where the inline function was invoked.

Example with Return in Lambda:

				
					var lambda = {
    println("Lambda expression")
    return  // Normally, returning from a lambda like this causes a compile-time error
}

fun main(args: Array<String>) {
    lambda()
}

				
			

Output:

				
					Error: 'return' is not allowed here

				
			

Using Return in Lambda Inside an Inline Function:

				
					fun main(args: Array<String>) {
    println("Main function starts")
    inlinedFunc({
        println("Lambda expression 1")
        return  // Inline functions allow this
    }, {
        println("Lambda expression 2")
    })
    println("Main function ends")
}

inline fun inlinedFunc(lmbd1: () -> Unit, lmbd2: () -> Unit) {
    lmbd1()
    lmbd2()
}

				
			

Output:

				
					Main function starts
Lambda expression 1

				
			
crossinline Keyword

By default, using return in an inline function exits both the lambda and the enclosing function. To prevent this behavior, you can mark the lambda with the crossinline keyword, which prevents return statements within the lambda.

Example with crossinline:

				
					fun main(args: Array<String>) {
    println("Main function starts")
    inlinedfunc({
        println("Lambda expression 1")
        return  // This will cause a compile-time error
    }, {
        println("Lambda expression 2")
    })
    println("Main function ends")
}

inline fun inlinedfunc(crossinline lmbd1: () -> Unit, lmbd2: () -> Unit) {
    lmbd1()
    lmbd2()
}

				
			

Output:

				
					Error: 'return' is not allowed here

				
			
noinline Keyword

If you only want some lambdas passed to an inline function to be inlined, you can use the noinline modifier. This tells the compiler not to inline certain lambdas.

Example with noinline:

				
					fun main(args: Array<String>) {
    println("Main function starts")
    inlinedFunc({
        println("Lambda expression 1")
        return
    }, {
        println("Lambda expression 2")
        return  // This causes a compile-time error
    })
    println("Main function ends")
}

inline fun inlinedFunc(lmbd1: () -> Unit, noinline lmbd2: () -> Unit) {
    lmbd1()
    lmbd2()
}

				
			

Output:

				
					Error: 'return' is not allowed here

				
			
Reified Type Parameters

Sometimes, you may need to access the type of a parameter passed during a function call. By using the reified modifier, you can retrieve the type of the parameter at runtime.

Example with reified:

				
					fun main(args: Array<String>) {
    genericFunc<String>()
}

inline fun <reified T> genericFunc() {
    println(T::class)
}

				
			

Output:

				
					class kotlin.String

				
			
Inline Properties

Inline properties work similarly to inline functions. The inline modifier can be used on property accessors (getter and setter) to copy the accessor methods to the calling location.

Example with Inline Properties:

				
					fun main(args: Array<String>) {
    print(flag)
}

fun foo(i: Int): Int {
    return i
}

inline var flag: Boolean
    get() = foo(10) == 10
    set(value) { /* Do nothing */ }

				
			

Output:

				
					true

				
			

Higher-Order Functions

Kotlin has excellent support for functional programming. Functions in Kotlin can be stored in variables, passed as arguments, and returned from other functions, making it easy to work with higher-order functions.

Higher-Order Functions

A higher-order function is a function that takes another function as a parameter or returns a function as its result. Instead of passing primitive data types (e.g., Int, String, Array), we pass anonymous functions or lambda expressions to other functions for more flexible and reusable code.

Passing Lambda Expressions to Higher-Order Functions

Kotlin allows us to pass lambda expressions as parameters to higher-order functions. These lambda expressions can either return Unit or any other type such as Int or String.

Example 1: Lambda Expression Returning Unit

				
					// Lambda expression
val lambda = { println("HelloKotlin") }

// Higher-order function accepting a lambda
fun higherfunc(lmbd: () -> Unit) {
    lmbd()  // Invokes the lambda
}

fun main(args: Array<String>) {
    higherfunc(lambda)  // Passing the lambda as a parameter
}

				
			

Output:

				
					HelloKotlin

				
			
Kotlin and Functional Programming: Higher-Order Functions

Kotlin has excellent support for functional programming. Functions in Kotlin can be stored in variables, passed as arguments, and returned from other functions, making it easy to work with higher-order functions.

Higher-Order Functions

A higher-order function is a function that takes another function as a parameter or returns a function as its result. Instead of passing primitive data types (e.g., Int, String, Array), we pass anonymous functions or lambda expressions to other functions for more flexible and reusable code.

Passing Lambda Expressions to Higher-Order Functions

Kotlin allows us to pass lambda expressions as parameters to higher-order functions. These lambda expressions can either return Unit or any other type such as Int or String.

Example 1: Lambda Expression Returning Unit

				
					// Lambda expression
val lambda = { println("HelloKotlin
") }

// Higher-order function accepting a lambda
fun higherfunc(lmbd: () -> Unit) {
    lmbd()  // Invokes the lambda
}

fun main(args: Array<String>) {
    higherfunc(lambda)  // Passing the lambda as a parameter
}

				
			

Output:

				
					HelloKotlin

				
			

Explanation:

  • The lambda expression prints a string.
  • The higher-order function higherfunc accepts this lambda and invokes it inside the function body.

Example 2: Lambda Expression Returning an Integer

				
					// Lambda expression returning an integer
val lambda = { a: Int, b: Int -> a + b }

// Higher-order function accepting the lambda
fun higherfunc(lmbd: (Int, Int) -> Int) {
    val result = lmbd(2, 4)  // Invokes the lambda by passing parameters
    println("The sum of two numbers is: $result")
}

fun main(args: Array<String>) {
    higherfunc(lambda)  // Passing the lambda as a parameter
}

				
			

Output:

				
					The sum of two numbers is: 6

				
			
Kotlin and Functional Programming: Higher-Order Functions

Kotlin has excellent support for functional programming. Functions in Kotlin can be stored in variables, passed as arguments, and returned from other functions, making it easy to work with higher-order functions.

Higher-Order Functions

A higher-order function is a function that takes another function as a parameter or returns a function as its result. Instead of passing primitive data types (e.g., Int, String, Array), we pass anonymous functions or lambda expressions to other functions for more flexible and reusable code.

Passing Lambda Expressions to Higher-Order Functions

Kotlin allows us to pass lambda expressions as parameters to higher-order functions. These lambda expressions can either return Unit or any other type such as Int or String.

Example 1: Lambda Expression Returning Unit

 

				
					// Regular function definition
fun printMe(s: String) {
    println(s)
}

// Higher-order function accepting another function as a parameter
fun higherfunc(str: String, myfunc: (String) -> Unit) {
    myfunc(str)  // Invokes the function passed as a parameter
}

fun main(args: Array<String>) {
    higherfunc("HelloKotlin", ::printMe)
}

				
			

Output:

				
					HelloKotlin

				
			
Returning a Function from a Higher-Order Function

Higher-order functions can also return a function. When returning a function, we need to specify the return type as a function signature.

Example: Returning a Function

				
					// Function declaration
fun mul(a: Int, b: Int): Int {
    return a * b
}

// Higher-order function returning another function
fun higherfunc(): (Int, Int) -> Int {
    return ::mul
}

fun main(args: Array<String>) {
    val multiply = higherfunc()  // Storing the returned function in a variable
    val result = multiply(2, 4)  // Invoking the returned function
    println("The multiplication of two numbers is: $result")
}

				
			

Output:

				
					The multiplication of two numbers is: 8