Short Excerpts

Short Insights on Previously Covered Random Topics

Shadowing Lambda Parameters in Kotlin

How Shadowing Lambda Parameters in Kotlin Affects Readability and Debugging

Kotlin is a powerful and expressive language that makes coding both enjoyable and efficient. However, even the most elegant languages have subtle pitfalls that can catch developers off guard. One such hidden issue is shadowing lambda parameters. In this blog, we’ll explore what shadowing means in Kotlin, why it happens, how it impacts code readability and debugging, and best practices to avoid potential issues.

What Is Shadowing?

Shadowing occurs when a variable declared inside a block has the same name as a variable from an outer scope. This inner variable hides or shadows the outer one, making it inaccessible within the block.

In Kotlin, shadowing can also happen inside lambdas, leading to unexpected behavior and debugging challenges.

Shadowing Lambda Parameters in Kotlin: A Hidden Readability Issue

Now, let’s see how shadowing applies to lambda parameters and why it can be problematic.

Let’s start with an example:

Kotlin
fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val sum = numbers.fold(0) { acc, number ->
        val number = acc + number // Shadowing `number`
        number
    }
    println(sum) // Output: 15
}

At first glance, this code seems straightforward. However, notice the val number = acc + number line. Here, the parameter number inside the lambda is being shadowed by a new local variable with the same name.

While the code works, it is bad practice because it makes the code harder to read. This can be confusing, especially in larger codebases where readability is crucial.

Debugging Complexity Due to Shadowing

Shadowing lambda parameters might not always lead to immediate bugs, but it can make debugging more difficult by obscuring the intended logic. Consider this example:

Kotlin
fun main() {
    val words = listOf("Hello", "Kotlin", "World")
    val result = words.fold("") { acc, word ->
        val word = acc + word.first() // Shadowing `word`
        word
    }
    println(result) // Output: "HKW"
}

Here, word.first() refers to the original lambda parameter, but since we immediately shadow word with a new variable, the parameter word is no longer accessible. This can lead to confusion when debugging because the scope of variables is not immediately clear.

How to Avoid Shadowing Lambda Parameters

To write clearer and more maintainable code, avoid shadowing lambda parameters by choosing distinct variable names.

Solution 1: Use a Different Variable Name

One of the simplest ways to avoid shadowing is to use a different name for new variables:

Kotlin
fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val sum = numbers.fold(0) { acc, num ->
        val newNumber = acc + num // Clearer naming
        newNumber
    }
    println(sum) // Output: 15
}

Here, renaming number to num and using newNumber prevents confusion.

Solution 2: Avoid Declaring Unnecessary Variables

If you don’t need an extra variable, you can return the computed value directly:

Kotlin
fun main() {
    val words = listOf("Hello", "Kotlin", "World")
    val result = words.fold("") { acc, word ->
        acc + word.first()
    }
    println(result) // Output: "HKW"
}

This keeps the code clean and avoids shadowing altogether.

When Shadowing Might Be Intentional

There are rare cases where shadowing can be useful, such as when dealing with scoped functions like let:

Kotlin
fun main() {
    val text: String? = "Hello"
    text?.let { text ->
        println(text.uppercase()) // Shadowing here is intentional
    }
}

In this case, the outer text is nullable, but inside let, we create a non-nullable text, making the code safer.

Dive deeper into this topic here: [Main Article URL]

Conclusion

Shadowing lambda parameters in Kotlin can lead to subtle bugs, reduce code readability, and complicate debugging. While Kotlin allows it, developers should strive for clear and maintainable code by avoiding unnecessary variable shadowing. Choosing distinct variable names and returning computed values directly can help make your code more understandable and easier to debug.

By being mindful of this hidden pitfall, you can write cleaner, more efficient Kotlin code while avoiding common mistakes.

Circular Dependencies

Resolving Circular Dependencies in Gradle: A Complete Guide

If you’re working on a multi-module Gradle project, encountering circular dependencies can be a frustrating roadblock. You may see an error like this:

Kotlin
Circular dependency between the following tasks:
:javaModule:compileJava
+--- :kotlinModule:compileJava
|    +--- :javaModule:compileJava (*)
|    \--- :kotlinModule:compileKotlin
|         \--- :javaModule:compileJava (*)
\--- :kotlinModule:compileKotlin (*)

This error occurs when two modules depend on each other, creating a loop that prevents Gradle from determining the correct build order.

Note- To recreate this scenario, we created two modules (javaModule and kotlinModule) and intentionally introduced a circular dependency.

In this guide we will cover the root cause of circular dependencies in Gradle and practical solutions to fix them.

Understanding the Problem: What Causes Circular Dependencies in Gradle?

A circular dependency occurs when two or more modules depend on each other, forming a loop that Gradle cannot resolve. In the error above:

  1. javaModule needs kotlinModule to compile.
  2. kotlinModule depends on javaModule to compile its Java code.
  3. kotlinModule:compileKotlin also requires javaModule, reinforcing the cycle.

Common Causes of Circular Dependencies

  • Bi-Directional Module Dependencies: If javaModule and kotlinModule both reference each other using implementation project(":eachOtherModule"), Gradle gets stuck in an infinite loop.
  • Transitive Dependencies: A third-party dependency might be causing an indirect loop between your modules.
  • Improper Task Dependencies: Gradle’s build order may be incorrectly configured, forcing modules to compile in a conflicting sequence.

How to Fix Circular Dependencies in Gradle

1. Remove Direct Bi-Directional Dependencies

The most common cause of circular dependencies is when two modules depend on each other directly.

Check your build.gradle or build.gradle.kts files:

Kotlin
// javaModule's build.gradle.kts
dependencies {
    implementation(project(":kotlinModule")) // Java module depends on Kotlin module
}

// kotlinModule's build.gradle.kts
dependencies {
    implementation(project(":javaModule")) // Kotlin module depends on Java module (Creates a cycle)
}

Since both modules reference each other, Gradle cannot determine the correct build order.

Solution: Remove one of the dependencies or refactor the project.

2. Use api Instead of implementation for Interface Dependencies

If a module only requires interfaces or abstract classes from another module, use api instead of implementation.

Kotlin
// build.gradle.kts
dependencies {
    api(project(":javaModule")) // Instead of implementation
    // ... other dependencies
}

This allows dependent modules to access the necessary classes without forcing a full compile-time dependency.

3. Introduce a Common Module

If both javaModule and kotlinModule share some code, it’s best to move that code into a separate module (e.g., commonModule).

New Project Structure:

Kotlin
commonModule
├── javaModule (depends on commonModule)
├── kotlinModule (depends on commonModule)

Update build.gradle.kts files accordingly:

javaModule/build.gradle.kts

Kotlin
// build.gradle.kts
dependencies {
    implementation(project(":commonModule"))
    // ... other dependencies
}

kotlinModule/build.gradle.kts

Kotlin
// build.gradle.kts
dependencies {
    implementation(project(":commonModule"))
    // ... other dependencies
}

Now, both javaModule and kotlinModule rely on commonModule, breaking the circular dependency.

4. Use Gradle Dependency Constraints

If you suspect a transitive dependency is causing the issue, use constraints in dependencies:

Kotlin
dependencies {
    constraints {
        implementation(project(":javaModule")) {
            because("Avoids circular dependency with kotlinModule")
        }
    }
}

This helps Gradle prioritize dependencies correctly.

5. Exclude Unnecessary Transitive Dependencies

If kotlinModule indirectly depends on javaModule due to a third-party library, exclude it:

Kotlin
dependencies {
    implementation(project(":javaModule")) {
        exclude(module = "kotlinModule") // Prevents circular dependency
    }
}

This prevents Gradle from resolving unnecessary dependencies that might create loops.

6. Adjust Compilation Task Dependencies

By default, Kotlin and Java modules in Gradle may have incorrect build order assumptions. You can explicitly set task dependencies:

Kotlin
tasks.named("compileKotlin") {
    dependsOn(tasks.named("compileJava").get())
}

This ensures Kotlin compilation happens after Java compilation, resolving potential conflicts.

Final Checks: Debugging Dependencies

After making changes, run the following command to inspect your project’s dependency tree:

Kotlin
./gradlew dependencies

Lists all dependencies in the project, including:

  • Direct dependencies
  • Transitive dependencies
  • Dependency versions
  • Resolved dependency graph
  • Helps debug dependency issues, such as conflicts or unexpected transitive dependencies.

If a circular dependency still exists, this command will highlight the problematic modules.

Conclusion

Circular dependencies in Gradle can cause major build issues, but they can be resolved using the right strategies. The best approach depends on the cause:

  • If modules depend on each other directly, remove one of the dependencies.
  • If shared code is needed, move it to a commonModule.
  • If the issue is caused by transitive dependencies, use exclude or constraints.
  • If compilation order is incorrect, manually adjust tasks.named("compileKotlin").

By following these steps, you can eliminate circular dependencies and improve the maintainability of your Gradle projects.

inline properties

How Inline Properties Improve Performance in Kotlin

Kotlin is known for its modern and expressive syntax, making development smoother and more efficient. One of the lesser-known but powerful features of Kotlin is inline properties, which can significantly impact performance. In this article, we’ll explore how inline properties improve performance in Kotlin, understand their implementation with examples, and see why they matter in real-world applications.

What Are Inline Properties?

Inline properties in Kotlin allow property accessors (getters and setters) to be inlined at the call site. This eliminates method call overhead and improves execution speed. Essentially, when you mark a property accessor as inline, the compiler replaces the function call with its actual code during compilation, resulting in a more efficient bytecode.

Why Use Inline Properties?

When you define a property with custom getters or setters in Kotlin, the compiler generates a function behind the scenes. Every time the property is accessed, this function is called. While this is usually not a problem, it can introduce unnecessary method call overhead, especially in performance-critical applications. Inline properties help optimize performance by removing this overhead.

Syntax of Inline Properties

To make a property’s accessor inline, you use the inline modifier with the getter or setter:

Kotlin
class User(val firstName: String, val lastName: String) {
    val fullName: String
        inline get() = "$firstName $lastName"
}

fun main() {
    val user = User("amol", "pawar")
    println(user.fullName)  // Output: amol pawar
}

Here,

  • The fullName property has a custom getter that concatenates firstName and lastName.
  • The inline keyword ensures that when fullName is accessed, the compiler replaces the function call with its actual expression.
  • This improves performance by avoiding a method call at runtime.

How Inline Properties Improve Performance

Reduces Method Call Overhead

When a property accessor is inlined, the function call is replaced with the actual code. This removes the overhead of method calls, reducing execution time.

Without Inline Properties

Kotlin
class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = width * height
}

fun main() {
    val rect = Rectangle(5, 10)
    println(rect.area)  // Generates a method call
}
  • Here, area is accessed using a generated getter function, which results in a method call.

With Inline Properties

Kotlin
class Rectangle(val width: Int, val height: Int) {
    val area: Int
        inline get() = width * height
}

fun main() {
    val rect = Rectangle(5, 10)
    println(rect.area)  // No method call, inlined at compile-time
}
  • The getter is inlined, meaning the multiplication happens directly where area is accessed, eliminating an extra function call.

Better Performance in Loops

If a property is accessed multiple times in a loop, an inline getter prevents redundant function calls, optimizing performance.

Kotlin
class Person(val age: Int) {
    val isAdult: Boolean
        inline get() = age >= 18
}

fun main() {
    val people = List(1_000_000) { Person(it % 50) }
    val adults = people.count { it.isAdult }  // More efficient with inline properties
    println("Number of adults: $adults")
}
  • With inline properties, isAdult is evaluated without generating function calls in each iteration, making large computations faster.

Reduces Bytecode Size

Inlining properties reduces the number of generated methods, resulting in smaller bytecode size and potentially lower memory usage.

When to Use Inline Properties

They are beneficial when:

  • The property is frequently accessed and has a simple getter or setter.
  • You want to optimize performance in high-frequency operations like loops.
  • You need to eliminate function call overhead for small computations.

However, avoid using them when:

  • The getter or setter contains complex logic.
  • The property returns large objects (inlining could increase code size).
  • The function involves recursion (since inline functions cannot be recursive).

Best Practices for Inline Properties

  1. Use them for lightweight operations — Keep the logic simple to maximize performance gains.
  2. Avoid inline properties with large return types — This can lead to increased bytecode size.
  3. Test performance improvements — Profile your code to ensure that inlining provides actual benefits.
  4. Be mindful of code readability — Excessive inlining can make debugging harder.

Explore the complete breakdown here: [Main Article URL]

Conclusion

Inline properties in Kotlin are a simple yet powerful feature that can improve performance by removing method call overhead, reducing bytecode size, and optimizing loops. While they should be used wisely, they offer significant benefits in performance-critical applications.

By understanding and leveraging inline properties, you can write more efficient Kotlin code, ensuring faster execution and better resource utilization.

stress testing

Stress Testing in Concurrent Programming (Kotlin): A Deep Dive

In software development, ensuring that applications run smoothly under normal conditions isn’t enough. Systems often face extreme workloads, concurrent processing, and unexpected spikes in demand. This is where stress testing comes into play.

Stress testing helps uncover performance bottlenecks, concurrency issues, and system stability problems that might not be evident under standard usage.

In this blog, we’ll dive deep into what stress testing is, why it’s important, and how it can help identify concurrency issues in multi-threaded applications. We’ll also explore how to fix race conditions using atomic variables in Kotlin and discuss best practices for stress testing.

What Is Stress Testing?

Stress testing is a technique used to evaluate how an application behaves under extreme conditions. This could involve:

  • High CPU usage
  • Memory exhaustion
  • Concurrent execution of multiple threads
  • Processing large amounts of data

The goal is to identify points of failure, performance degradation, or unexpected behavior that might not surface under normal conditions.

Key Objectives of Stress Testing

Detect Concurrency Issues (Race Conditions, Deadlocks, Thread Starvation)

  • Ensures that shared resources are managed correctly in a multi-threaded environment.

Measure System Stability Under High Load

  • Determines if the application remains functional and doesn’t crash or slow down under stress.

Identify Performance Bottlenecks

  • Highlights areas where performance can degrade when the system is heavily loaded.

Ensure Correctness in Edge Cases

  • Helps expose unpredictable behaviors that don’t appear during regular execution.

Concurrency Issues in Multi-Threaded Applications

Concurrency bugs are notoriously difficult to detect because they often appear only under high load or specific timing conditions. One of the most common issues in concurrent programming is race conditions.

Example: Race Condition in Kotlin

Consider a shared counter variable accessed by multiple threads:

Kotlin
var sharedCount = 0

fun main() {
    val workers = List(1000) {
        Thread { sharedCount++ }
    }
    
    workers.forEach { it.start() }
    workers.forEach { it.join() }
    
    println(sharedCount) // Unpredictable result
}

Why Is This Problematic?

  • The sharedCount++ operation is not atomic (it consists of three steps: read, increment, and write).
  • Multiple threads may read the same value, increment it, and write back an incorrect value.
  • Due to context switching, some increments are lost, leading to an unpredictable final result.

Expected vs. Actual Output

Expected Result (In Ideal Case): 1000

but in most cases, 

Actual Result (Most Cases): Less than 1000 due to lost updates.

Detecting This Issue with a Stress Test

To reliably expose the race condition, increase the number of threads and iterations:

Kotlin
var sharedCount = 0

fun main() {
    val workers = List(10000) {
        Thread {
            repeat(100) { sharedCount++ }
        }
    }
    
    workers.forEach { it.start() }
    workers.forEach { it.join() }
    
    println(sharedCount) // Unpredictable, usually much less than 1,000,000
}

How to Fix This? Using Atomic Variables

To ensure correctness, Kotlin provides AtomicInteger, which guarantees atomicity of operations.

Kotlin
import java.util.concurrent.atomic.AtomicInteger

val sharedCount = AtomicInteger(0)

fun main() {
    val workers = List(10000) {
        Thread {
            repeat(100) { sharedCount.incrementAndGet() }
        }
    }

    workers.forEach { it.start() }
    workers.forEach { it.join() }

    println(sharedCount.get()) // Always 1,000,000
}

Why Does AtomicInteger Work?

  • incrementAndGet() is atomic, meaning it ensures that updates occur without interference from other threads.
  • No values are lost, and the result is always deterministic and correct.

Other Common Stress Testing Scenarios

Deadlocks

A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource.

Example: Deadlock Scenario

Kotlin
val lock1 = Any()
val lock2 = Any()

fun main() {
    val thread1 = Thread {
        synchronized(lock1) {
            Thread.sleep(100)
            synchronized(lock2) {
                println("Thread 1 acquired both locks")
            }
        }
    }

    val thread2 = Thread {
        synchronized(lock2) {
            Thread.sleep(100)
            synchronized(lock1) {
                println("Thread 2 acquired both locks")
            }
        }
    }

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()
}

Result: The program will hang indefinitely because each thread is waiting for the other to release a lock.

Solution: Always acquire locks in a consistent order to prevent circular waiting and potential deadlocks. If possible, use timeouts or lock hierarchies to further minimize the risk.

Best Practices for Stress Testing

Test Under High Load

  • Simulate thousands or millions of concurrent operations to uncover hidden issues.

Use Thread-Safe Data Structures

  • Kotlin provides AtomicInteger, ConcurrentHashMap, and CopyOnWriteArrayList for safer multi-threading.

Monitor Performance Metrics

  • Use profiling tools like VisualVM or Kotlin Coroutines Debugging tools to track CPU, memory, and execution time during stress tests.

Run Tests Repeatedly

  • Some concurrency bugs appear only occasionally, so rerun tests multiple times.

Conclusion

Stress testing is a crucial technique for ensuring software stability, performance, and correctness under extreme conditions. It helps identify concurrency issues like race conditions and deadlocks that might not be obvious during normal execution.

By using atomic variables and thread-safe practices, developers can write more reliable multi-threaded applications. If you’re building high-performance or concurrent software, incorporating stress testing in your workflow will save you from unexpected failures and unpredictable behavior in production.

Reversing Words in kotlin String

Reversing Words in a String Using Kotlin: A Detailed Guide

Reversing words in a sentence is a common programming task often used in coding interviews and algorithm-based challenges. In this blog, we’ll break down a Kotlin function that reverses the order of words in a given string. We’ll analyze each part of the code, identify potential improvements, and present an optimized version.

Reversing Words: Problem Statement

Given an input string containing multiple words separated by spaces, we need to reverse the order of words while maintaining their original form.

Input:

Kotlin
"Hello India Pune World"

Output:

Kotlin
"World Pune India Hello"

Understanding the Kotlin Code

Let’s analyze the following Kotlin function that reverses the order of words in a string:

Kotlin
fun reverseInputStatement(input: String): String {
    val wordList = input.split(" ")
    val mutableWordList = wordList.toMutableList()
    
    var indexFromEnd = mutableWordList.size - 1
    
    for (indexFromStart in mutableWordList.indices) {
        if (indexFromStart < indexFromEnd) {
            val temp = mutableWordList[indexFromStart]
            mutableWordList[indexFromStart] = mutableWordList[indexFromEnd]
            mutableWordList[indexFromEnd] = temp
        }
        indexFromEnd -= 1
    }
    
    return mutableWordList.toString()
}

Step-by-Step Breakdown

Step 1: Splitting the Input String into Words

Kotlin
val wordList = input.split(" ")
  • The split(" ") function divides the input string into a list of words based on spaces.
  • For the input “Hello India Pune World”, the output will be:
  • ["Hello", "India", "Pune", "World"]

Step 2: Converting to a Mutable List

Kotlin
val mutableWordList = wordList.toMutableList()   
  • Since lists in Kotlin are immutable by default, we convert it to a mutable list to allow modifications.

Step 3: Swapping Words to Reverse Their Order

Kotlin
var indexFromEnd = mutableWordList.size - 1
  • indexFromEnd is initialized to the last index of the list.

The loop performs word swaps to reverse the order:

Kotlin
for (indexFromStart in mutableWordList.indices) {
    if (indexFromStart < indexFromEnd) {
        val temp = mutableWordList[indexFromStart]
        mutableWordList[indexFromStart] = mutableWordList[indexFromEnd]
        mutableWordList[indexFromEnd] = temp
    }
    indexFromEnd -= 1
}

Here,

  • The loop iterates through the list from both the beginning (indexFromStart) and the end (indexFromEnd).
  • The words are swapped until they meet in the middle.

Here is the how the list changes at each iteration:

Before swapping: ["Hello", "India", "Pune", "World"]

  • Step 1: Swap Hello and World["World", "India", "Pune", "Hello"]
  • Step 2: Swap India and Pune["World", "Pune", "India", "Hello"]

Loop stops as words are fully reversed.

Step 4: Converting Back to a String

Kotlin
return mutableWordList.toString()

This returns the reversed list, but the output will be formatted as:

Kotlin
[World, pune, india, Hello]

This isn’t a properly formatted sentence, so let’s fix this issue.

Optimizing the Code

The existing function works, but it can be significantly simplified using Kotlin’s built-in functions:

Kotlin
fun reverseInputStatement(input: String): String {
    return input.split(" ").reversed().joinToString(" ")
}

Why is This Better?

  • split(" "): Splits the input string into a list of words.
  • reversed(): Reverses the list.
  • joinToString(" "): Joins the list back into a properly formatted string.
  • More readable and concise compared to manually swapping elements.

Final Output

For the input:

Kotlin
"Hello India Pune World"

The output will be:

Kotlin
"World Pune India Hello"

Key Takeaways

  1. Use Kotlin’s built-in functions (split(), reversed(), joinToString()) for cleaner and more efficient code.
  2. Avoid unnecessary manual swapping unless specifically required.
  3. Understand how lists work — mutable and immutable lists impact how you modify data in Kotlin.
  4. Code readability is important — the optimized version is much easier to understand and maintain.

Conclusion

Reversing words in a string is a simple yet insightful exercise in Kotlin. The initial approach using a loop and swapping elements works but is not the most efficient solution. By leveraging Kotlin’s built-in functions, we can achieve the same result with cleaner and more readable code.

Understanding such basic transformations is crucial for improving problem-solving skills, especially in coding interviews and real-world applications.

tailrec modifier

How Kotlin’s tailrec Modifier Optimizes Recursive Functions

Recursion is a powerful concept in programming that allows functions to call themselves to solve problems. However, recursive functions can sometimes lead to stack overflow errors if they have deep recursion levels.

Kotlin provides the tailrec modifier to optimize recursive functions and convert them into an efficient iterative version under the hood. This helps in reducing memory consumption and avoiding stack overflow errors.

In this post, we’ll explore how Kotlin’s tailrec modifier works, when to use it, and see examples with explanations.

Understanding tailrec modifier, Recursive Functions and Tail Recursive Function

A recursive function is one that calls itself to solve a problem by breaking it down into smaller instances. While recursion provides an elegant solution, deep recursion can lead to excessive memory usage due to function call stack frames.

A tail-recursive function is a special type of recursive function where the recursive call is the last operation before returning a result. This allows the compiler to optimize it into a loop, eliminating the need for additional stack frames and improving efficiency.

In Kotlin, recursive functions are fully supported, and the tailrec modifier can be used to optimize tail-recursive functions. Let’s explore these concepts with examples of factorial and Fibonacci calculations.

Factorial Function

Imperative Implementation

Kotlin
fun factorial(n: Long): Long {
    var result = 1L
    for (i in 1..n) {
        result *= i
    }
    return result
}

This is a straightforward imperative implementation of the factorial function using a for loop to calculate the factorial of a given number n.

Recursive Implementation

Kotlin
fun functionalFactorial(n: Long): Long {
    tailrec fun go(n: Long, acc: Long): Long {
        return if (n <= 0) {
            acc
        } else {
            go(n - 1, n * acc)
        }
    }
    return go(n, 1)
}

In the recursive version, we use an internal recursive function go that calls itself until a base condition (n <= 0) is reached. The accumulator (acc) is multiplied by n at each recursive step.

Tail-Recursive Implementation

Kotlin
fun tailrecFactorial(n: Long): Long {
    tailrec fun go(n: Long, acc: Long): Long {
        return if (n <= 0) {
            acc
        } else {
            go(n - 1, n * acc)
        }
    }
    return go(n, 1)
}

The tail-recursive version is similar to the recursive one, but with the addition of the tailrec modifier. This modifier informs the compiler that the recursion is tail-recursive, allowing for optimization.

Fibonacci Function

Imperative Implementation

Kotlin
fun fib(n: Long): Long {
    return when (n) {
        0L -> 0
        1L -> 1
        else -> {
            var a = 0L
            var b = 1L
            var c = 0L
            for (i in 2..n) {
                c = a + b
                a = b
                b = c
            }
            c
        }
    }
}

This is a typical imperative implementation of the Fibonacci function using a for loop to iteratively calculate Fibonacci numbers.

Recursive Implementation

Kotlin
fun functionalFib(n: Long): Long {
    fun go(n: Long, prev: Long, cur: Long): Long {
        return if (n == 0L) {
            prev
        } else {
            go(n - 1, cur, prev + cur)
        }
    }
    return go(n, 0, 1)
}

The recursive version uses an internal function go that recursively calculates Fibonacci numbers. The function maintains two previous values (prev and cur) during each recursive call.

Tail-Recursive Implementation

Kotlin

fun tailrecFib(n: Long): Long {
    tailrec fun go(n: Long, prev: Long, cur: Long): Long {
        return if (n == 0L) {
            prev
        } else {
            go(n - 1, cur, prev + cur)
        }
    }
    return go(n, 0, 1)
}

The tail-recursive version of the Fibonacci function, similar to the recursive one, benefits from the tailrec modifier for potential optimization.

Profiling with executionTime:

To test which implementation is faster, we can write a poor’s man profiler function:

Kotlin
fun executionTime(body: () -> Unit): Long {
    val startTime = System.nanoTime()
    body()
    val endTime = System.nanoTime()
    return endTime - startTime
}
Kotlin
fun main(args: Array<String>) {
    println("factorial: " + executionTime { factorial(20) })
    println("functionalFactorial: " + executionTime { functionalFactorial(20) })
    println("tailrecFactorial: " + executionTime { tailrecFactorial(20) })

    println("fib: " + executionTime { fib(93) })
    println("functionalFib: " + executionTime { functionalFib(93) })
    println("tailrecFib: " + executionTime { tailrecFib(93) })
}

This main function tests the execution time of each implementation using the executionTime function. It helps compare the performance of the imperative, recursive, and tail-recursive versions of both factorial and Fibonacci functions.

These execution times represent the time taken to run each function, providing insights into their relative performance. Please note that actual execution times may vary based on the specific environment and hardware.

The output of the profiling demonstrates that tail-recursive implementations, indicated by the tailrec modifier, are generally more optimized and faster than their purely recursive counterparts. However, it’s essential to note that tail recursion doesn’t automatically make the code faster in all cases, and imperative implementations might still outperform recursive ones. The choice between recursion and tail recursion depends on the specific use case and the characteristics of the problem being solved.

error: Content is protected !!