DEV Community

Cover image for Decoding Kotlin - Your guide to solving the mysterious in Kotlin
João Esperancinha
João Esperancinha

Posted on • Updated on

Decoding Kotlin - Your guide to solving the mysterious in Kotlin

On the 24th of April 2024, I gave a presentation about Decoding Kotlin at the JetBrains Headquarters in Amterdam, located close to the RAI:

Kotlin MeetUp Amsterdam @ JetBrains! Kotlin Notebooks, Decoding Kotlin & more!

I had a blast on that day and it was great to have met everyone, the different presenters, the organizers and of course the audience. We targeted our presentations to last 30 minutes and in general we succeeded in that. For my presentation it would have also been great to have been able to complement it with practical live examples. But that would have meant that the session would have had to go up to about 1 hour to be honest also considering the buffer needed to answer questions for the audience. If I consider the feedback I got from the audience, I have good confidence that I made my points across.

With this article I want to complement my presentation by explaining in detail the different examples I showed in the slides:

Decoding Kotlin - Your Guide To Solving The Mysterious in Kotlin | PDF | Computer Programming | Software Engineering

These are the slides of the presentation I gave at the JetBrains HQ by the RAI in Amsterdam named "Decoding Kotlin - Your Guide to Solving the Mysterious in Kotlin". I want to express a big thank you to Xebia and JetBrains for the opportunity. I gave this presentation on the 24th of April 2024.

favicon scribd.com

Before we continue I would recommend to checkout the repository called kotlin-mysteries on github. This is the project that supports this article and you can find it under my username jesperancinha over here:

GitHub logo jesperancinha / kotlin-mysteries

Discover 5 oddities about Kotlin that can occur in the workplace, your personal projects or that you may have come across in the community.

Kotlin Mysteries

Introduction

This repo is a list of examples in Kotlin that escape the norm. This is your guide to the mysterious and different in Kotlin.

Topics

  1. Nullability
    1. Working with the Spring Framework - carparts-manager
    2. Reflection to force nulls - carparts-null-play
  2. inline and cross-inline - carparts-inline
    1. The Java overview
  3. Tail recursive => Tail Cal Optimization (TCO) - carparts-tairec
    1. What is it
    2. Why?
    3. How it makes us work recursively and not use mutable
  4. Data classes - carparts-data-structures
    1. Why things work and why things don't work
    2. How to fix the ones that don't
    3. How to work with use-site targets.
  5. What does a delegate do? and other use-site targets.
    1. Delegates - carparts-use-site-targets
    2. Delegates with Spring - carparts-data-structures

About me

GitHub followers




Nullability - Whenever possible?

This is actually a hint to think a bit about what null-safety means in Kotlin. So let's recap about that:

Kotlin promises a guarantee of null-safety. Although we can use nullablemembers in our classes, we really shouldn’t whenever possible.

In practice null-safety in Kotlin means that we can guarantee that certain variables or class members are either nullable or non-nullable throught the course of the running code. I allows us to better predict our code and better localize NPE's (NullPointerException) whenever they occur. While this is true, null-safety, currently only works if we talk about assigning values to variables. There are however lots of examples where we can set the values, which has a subtle difference to assign a value. So let's have a look at an example that people will eventually run into when working with Kotlin and Spring on the server-side.


Let's have a look at the example. For our example we need a database. I provide that in the carparts-database-service. In this example I create a database schema called carparts and another called carparts-data-structures with a simple access using username admin and password admin. The database is created using a commonly known bash script that I provide in that directory called create-multiple-postgresql-databases.sh. Before we continue, let's just start the database and for that let's just use one of these commands:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

or just the simplified Makefile script using:

make dcup
Enter fullscreen mode Exit fullscreen mode

Once we have done that, it is probably best to start the service from your IDE. I use Intellij for that. The service is located at the directory carparts-manager on the root of the project. To start the service you can either run a simple command called gradle runBoot or just run the script on the Makefile I made for it and you can call it using make run. Remember that the service cannot start if the database isn't up and running.


Flyway will run and it will execute this script first:

CREATE SEQUENCE car_parts_id_sequence
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

CREATE TABLE CAR_PARTS (
    id BIGINT NOT NULL DEFAULT nextval('car_parts_id_sequence'::regclass),
    name VARCHAR(100),
    production_date timestamp,
    expiry_date timestamp,
    bar_code BIGINT,
    cost float
);
Enter fullscreen mode Exit fullscreen mode

This script creates a simple database similar to what we may find in production environments these days. It is important to notice that all of the columns, except for the id column can be null in this case. Pay special attention to the column name. This is very important for this example.

The second script to run gives us some data and it is this one:

INSERT INTO CAR_PARTS
    (name, production_date, expiry_date, bar_code, cost)
VALUES ('screw', current_date, current_date, 12345, 1.2);
INSERT INTO CAR_PARTS
    (name, production_date, expiry_date, bar_code, cost)
VALUES (null, current_date, current_date, 12345, 1.2);
Enter fullscreen mode Exit fullscreen mode

We can observe now that perhaps by mistake or any other reason, we didn't put a name to the second car part. Instead, we are only forcing it to be null.

In our example a simple way to map this database to our Kotlin code is to create an entity like this one in its very, very basic form:

@Table(name = "CAR_PARTS")
@Entity
data class CarPart(
    @Id
    val id: Long,
    val name: String,
    val productionDate: Instant,
    val expiryDate: Instant,
    val barCode: Long,
    val cost: BigDecimal
)
Enter fullscreen mode Exit fullscreen mode

This is a typical first approach to mapping the entity. We simply define it also to be a table under a specific name, in this case CAR_PARTS, we mark it to be an @Entity and then finally, we declare all the columns in the form of Kotlin types. They are all type safe, immutable and non-nullable class members. But if you recall from before, we did put a null value into the second element of our table CAR_PARTS.

But before we play with our running service, let's create a simple test to checkout what happens thinking that somehow, someway, we will get both parts. One example of such test could be this one:

class DomainTest : ContainerTest() {

    @Autowired
    private lateinit var carPartDao: CarPartDao

    @Test
    fun `should mysteriously get a list with a car part with a name null`() {
        // For illustration purposes only.
        // It DOES receive a null value.
        carPartDao.findAll()
            .filter {
                it.name == null
            }
            .shouldHaveSize(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

On this test we are only checking if we will get any element of the a list coming from findAll that has a null value for name, which seemingly should not work.

What could be the result of such a test?

In most cases, people coming for the first time starting to code in Kotlin will not even think of such possibility given that Kotlin applies null-safety as one of its core principles. Having said that, we frequently don't really phrase out exactly what that means. Null-safety means that we can predict just by looking at the declaration of a class member of a variable, that its type is nullable or not. But this applies only to assigning. Having this in mind let's run this test and checkout the result:

Test result

IntelliJ will let you know that the compiler finds the assertion we are making completely useless. However, as we can see from the resulting test, the non-nullable class member called name now carries a null value. This is something that, if you didn't know about it, you probably didn't expected this. The reason for this is that val and non-nullables is something that the JVM just doesn't understand and with reflection we can still do whatever we want with variables. Under the hood, in its depths, that is what frameworks like the Spring Framework use to be able to perform IoC, validations and use AOP (Aspect Oriented Programming).

From this, we can imagine that using a Spring MVC design pattern the runtime will fail, but not exactly while we read the data from the database. MVC usually implies that, at the service layer, calls to converters from entity to dto and vice-versa will take place. It is here where the problem starts. For many that do not know about this phenomenon, this is exactly where the problem begins. We have learned about null safety, but now we get a difficult to interpret NPE at the service level, where we do not expect an NPE at all given our first impression of what null-safety means. Many people leave investigating if the data is actually being returned null from the database simply because that is exactly what they do not expect.

To illustrate this problem I have created a service with an explicit use of a try-catch exactly during conversion and that logs this when we make a request to list our data. The idea is to log all errors and exclude that data that cannot be converted from an entity to a Dto:

fun CarPart.toDto() = try {
    CarPartDto(
        id = id,
        name = name,
        productionDate = productionDate,
        expiryDate = expiryDate,
        barCode = barCode,
        cost = cost
    )
} catch (ex: Exception) {
    logger.error("Failed to convert data!", ex)
    null
} finally {
    logger.info("Tried to convert data {}", this)
}
Enter fullscreen mode Exit fullscreen mode

We can now make a call to our running service to the list all endpoint. The implementation looks like this:

@RestController
@RequestMapping
class CarPartsController(
    val carPartsService: CarPartsService
) {
    @PostMapping("create")
    fun createCarPart(@RequestBody carPartDto: CarPartDto) = carPartsService.createCarPart(carPartDto)

    @PutMapping("upsert")
    fun upsert(@RequestBody carPartDto: CarPartDto) = carPartsService.upsertCarPart(carPartDto)

    @GetMapping
    fun getCarParts() = carPartsService.getCarParts()
}
Enter fullscreen mode Exit fullscreen mode

And to make a request, we can just use the scratch file I have placed at the carparts-manager service module:

###
GET http://localhost:8080/api/v1/carparts
Enter fullscreen mode Exit fullscreen mode

With this request, we should get a response like this:

[
  {
    "id": 1,
    "name": "screw",
    "productionDate": "2024-04-25T00:00:00Z",
    "expiryDate": "2024-04-25T00:00:00Z",
    "barCode": 12345,
    "cost": 1.2
  }
]
Enter fullscreen mode Exit fullscreen mode

This represents only the car part that actually has a name. The other one that didn't succeed during the conversion gets filtered out, however, nonetheless still gets logged out to the console:

java.lang.NullPointerException: Parameter specified as non-null is null: 
method org.jesperancinha.talks.carparts.carpartsmanager.dto.CarPartDto.<init>, 
parameter name

        at 
(...)

2024-04-25T21:13:52.316+02:00  INFO 151799 --- [carparts-manager] [nio-8080-exec-1] o.j.t.c.c.converter.Converters           

: 

Tried to convert data 

CarPart(id=2, name=null, productionDate=2024-04-25T00:00:00Z, expiryDate=2024-04-25T00:00:00Z, barCode=12345, cost=1.2)
Enter fullscreen mode Exit fullscreen mode

And here we can see that the data is coming through to our runtime anyway. It is only at the moment that we try to assign a null value via our own code, that we get a NPE, which is, in this case, at the time we try to convert our entity to a DTO.


The Spring Framework, like many other frameworks, resort to reflection to be able to inject values into the field and class members of our instances and reflection is a part of how these frameworks handle objects. But to focus precisely on where this phenomenon happens we can have a look a at a simpler example that shows a direct way of setting a null value into a data class in kotlin:

data class CarPartDto(
    val id: Long,
    val name: String,
    val productionDate: Instant,
    val expiryDate: Instant,
    val barCode: Long,
    val cost: BigDecimal
)
Enter fullscreen mode Exit fullscreen mode

This example can be found on module carparts-null-play. In this class I declared all class members to be non-nullable, but, as mentioned above, we can still set a null value to name:

fun main() {
    val carPartDto = CarPartDto(
        id = 123L,
        name = "name",
        productionDate = Instant.now(),
        expiryDate = Instant.now(),
        cost = BigDecimal.TEN,
        barCode = 1234L
    )
    println(carPartDto)
    val field: Field = CarPartDto::class.java
        .getDeclaredField("name")
    field.isAccessible = true
    field.set(carPartDto, null)
    println(carPartDto)
    assert(carPartDto.name == null)
    println(carPartDto.name == null)
}
Enter fullscreen mode Exit fullscreen mode

If we run this program, we will get this result in the console:

CarPartDto(id=123, name=name, productionDate=2024-04-26T06:45:51.335693902Z, expiryDate=2024-04-26T06:45:51.335696975Z, barCode=1234, cost=10)
CarPartDto(id=123, name=null, productionDate=2024-04-26T06:45:51.335693902Z, expiryDate=2024-04-26T06:45:51.335696975Z, barCode=1234, cost=10)
true
Enter fullscreen mode Exit fullscreen mode

And this just proves the point that although null-safety is a big a concern in Kotlin and it makes the code more readable and more predictable, it doesn't play a role in setting a value, while it does play 100% a role in assigning a value.


There are multiple ways to approach this problem, when it arises, especially when we are using frameworks that provision CRUD interfaces and it is a good idea to just mention a few of them:

  • Database Migration - If possible, make sure that there are no more null values in the database and create a constraint in the table itself to prevent nulls from being create
  • Handle the null values earlier - If nulls aren't expected, we could manually handle these null's. The downside is that your IDE will probably keep signalling a warning and you'll have to supress those warnings one way or the other if so.
  • Use another framework - This is probably a more costly operation and it is not always clear if using another framework will solve this specific problem

Inline and crossinline - Why does this matter

You probably have already heard of inline in Kotlin. If you haven't then understanding it is quite easy and to explain it, we can say something like this:

Inline and crossline can be used incombination with each other. Inline creates bytecode copies of the code per each call point and they can even help avoid type erasure. Crossinline improves readability and some safety, but nothing really functional

In this in my experience the problem wasn't so much to figure out how the code should compiler and where to use crossinline, but the question was why was the compiler asking us, the developers, to use crossinline. For this I have created another example located on module located at the root of the project. There, we can find this example:

fun main() {
    callEngineCrossInline {
        println("Place key in ignition")
        println("Turn key or press push button ignition")
        println("Clutch to the floor")
        println("Set the first gear")
    }.run { println(this) }
}

inline fun callEngineCrossInline(crossinline startManually: () -> Unit) {
    run loop@{
        println("This is the start of the loop.")
        introduction {
            println("Get computer in the backseat")
            startManually()
            return@introduction
        }
        println("This is the end of the loop.")
    }
    println("Engine started!")
}

fun introduction(intro: () -> Unit) {
    println(LocalDateTime.now())
    intro()
    return
}
Enter fullscreen mode Exit fullscreen mode

In this example, we are simply creating a program that calls a higher-order inline function called callEngineCrossInline that passes on an argument that is a function and that get's called inside of it via another higher order function, which is not inlined and receiver a new function that calls startManually as part of its body. The code does compiler and there does not seem to be a problem with it and here, we are using crossinline. But let's think about this a bit more in detail. The compiler is going to try to compile this and when we decompile that into Java, it creates something like this:

public final class IsolatedCarPartsExampleKt {
   public static final void main() {
      int $i$f$callEngineCrossInline = false;
      int var1 = false;
      String var2 = "This is the start of the loop.";
      System.out.println(var2);
      introduction((Function0)(new IsolatedCarPartsExampleKt$main$$inlined$callEngineCrossInline$1()));
      var2 = "This is the end of the loop.";
      System.out.println(var2);
      String var4 = "Engine started!";
      System.out.println(var4);
      Unit var3 = Unit.INSTANCE;
      Unit $this$main_u24lambda_u241 = var3;
      int var6 = false;
      System.out.println($this$main_u24lambda_u241);
   }

   public static final void callEngineCrossInline(@NotNull final Function0 startManually) {
      Intrinsics.checkNotNullParameter(startManually, "startManually");
      int $i$f$callEngineCrossInline = false;
      int var2 = false;
      String var3 = "This is the start of the loop.";
      System.out.println(var3);
      introduction((Function0)(new Function0() {
         public final void invoke() {
            String var1 = "Get computer in the backseat";
            System.out.println(var1);
            startManually.invoke();
         }

         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }
      }));
      var3 = "This is the end of the loop.";
      System.out.println(var3);
      String var4 = "Engine started!";
      System.out.println(var4);
   }

   public static final void introduction(@NotNull Function0 intro) {
      Intrinsics.checkNotNullParameter(intro, "intro");
      LocalDateTime var1 = LocalDateTime.now();
      System.out.println(var1);
      intro.invoke();
   }

   public static void main(String[] args) {
      main();
   }
}

public final class IsolatedCarPartsExampleKt$main$$inlined$callEngineCrossInline$1 extends Lambda implements Function0 {
   public IsolatedCarPartsExampleKt$main$$inlined$callEngineCrossInline$1() {
      super(0);
   }

   public final void invoke() {
      String var1 = "Get computer in the backseat";
      System.out.println(var1);
      int var2 = false;
      String var3 = "Place key in ignition";
      System.out.println(var3);
      var3 = "Turn key or press push button ignition";
      System.out.println(var3);
      var3 = "Clutch to the floor";
      System.out.println(var3);
      var3 = "Set the first gear";
      System.out.println(var3);
   }

   public Object invoke() {
      this.invoke();
      return Unit.INSTANCE;
   }
}

Enter fullscreen mode Exit fullscreen mode

It is important to notice that callEngineCrossInline gets inlined up until the call to introduction and the function that we pass through via introduction gets also inlined. Now let' think about how would this have worked if we had used instead of return@introduction, something like return@loop or even return@callEngineCrossInline. Can you image how inline would have worked here? And if you can, do you see how complicated would that be to make generic for all kinds of functions or methods in Kotlin, that would make non-local returns? Neither do I and this is part of the reason why crossinline exists. In this specific case, if we do not use crossinline the compiler it will not allow us to build on the source code. It will mention that it is mandatory. But even if we try to make a non-local return in this case, the compiler will still fail saying that we are making a non-local return and so we will get these warnings respectively:

Image description

and

Image description

But the big question was, if the compiler already knows that non-local returns are a big problem in making inline code, or in other words, creating multiple copies of the bytecode per call point, why do we need to even put a crossinline before our parameter declaration? Maybe not in this case, but cross inline works like a standard and while in this specific cases it only guarantees an improvement in code readability, there are cases where crossinline actually has code safety functionality and for that I created this example:

object SpecialShopNonLocalReturn {

    inline fun goToStore(chooseItems: () -> Unit) {
        println("Walks in")
        chooseItems()
    }

    @JvmStatic
    fun main(args: Array<String> = emptyArray()) {
        goToStore {
            println("Make purchase")
            return@main
        }
        println("Never walks out")
    }
}
Enter fullscreen mode Exit fullscreen mode

This example is looks quite easy to understand and the compiler will show no problems with this. It simulates the idea of going into a store, purchasing some items and walking out of the store. But in this specific example we don't really walk out of the store.


If you are using Intellij, odds are, that you are not getting any warning with this code. In this code we see that with return@main we are making a non-local return to the main function. The results in the println("Never walks out") never even being called. And if we decompile the resulting byte code, we will find something interesting:

public final class SpecialShopNonLocalReturn {
   @NotNull
   public static final SpecialShopNonLocalReturn INSTANCE = new SpecialShopNonLocalReturn();

   private SpecialShopNonLocalReturn() {
   }

   public final void goToStore(@NotNull Function0 chooseItems) {
      Intrinsics.checkNotNullParameter(chooseItems, "chooseItems");
      int $i$f$goToStore = false;
      String var3 = "Walks in";
      System.out.println(var3);
      chooseItems.invoke();
   }

   @JvmStatic
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      SpecialShopNonLocalReturn this_$iv = INSTANCE;
      int $i$f$goToStore = false;
      String var3 = "Walks in";
      System.out.println(var3);
      int var4 = false;
      String var5 = "Make purchase";
      System.out.println(var5);
   }

   public static void main$default(String[] var0, int var1, Object var2) {
      if ((var1 & 1) != 0) {
         int $i$f$emptyArray = false;
         var0 = new String[0];
      }

      main(var0);
   }
}
Enter fullscreen mode Exit fullscreen mode

The compiler doesn't create bytecodes for the println("Never walks out") call. It essentially ignores this dead-code. This is where crossinline may be used and can be very helpful. Let's look at a variation of this example where we do get out of the store:

object SpecialShopLocalReturn {
    inline fun goToStore(crossinline block: () -> Unit) {
        println("Walks in")
        block()
    }

    @JvmStatic
    fun main(args: Array<String> = emptyArray()) {
        goToStore {
            println("Make purchase")
            return@goToStore
        }
        println("Walks out")
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, crossinline signas the compiler to check the code for non-local returns. In this case, if we try to make a non-local return, the compiler should warn you of an error, and if you are using IntelliJ for it, it will show something like this:

Image description

And in this case, we don't need to worry about the decompiled code or how this looks in the bytecode, because crossinline assures us that every call to goToStore will never accept a function that will return to @main:

public final class SpecialShopLocalReturn {
   @NotNull
   public static final SpecialShopLocalReturn INSTANCE = new SpecialShopLocalReturn();

   private SpecialShopLocalReturn() {
   }

   public final void goToStore(@NotNull Function0 block) {
      Intrinsics.checkNotNullParameter(block, "block");
      int $i$f$goToStore = false;
      String var3 = "Walks in";
      System.out.println(var3);
      block.invoke();
   }

   @JvmStatic
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      SpecialShopLocalReturn this_$iv = INSTANCE;
      int $i$f$goToStore = false;
      String var3 = "Walks in";
      System.out.println(var3);
      int var4 = false;
      String var5 = "Make purchase";
      System.out.println(var5);
      String var6 = "Walks out";
      System.out.println(var6);
   }

   public static void main$default(String[] var0, int var1, Object var2) {
      if ((var1 & 1) != 0) {
         int $i$f$emptyArray = false;
         var0 = new String[0];
      }

      main(var0);
   }
}
Enter fullscreen mode Exit fullscreen mode

So in this case we succeed to get out of the store. As we can see from all of the above crossinline doesn't really have a functional transformation functionality for the code. It is, instead, used as a marker to warn the developers of the code intent making it more readable and as shown in this last case, giving it some level of protection against developer mistakes. This doesn't stop a developer, not knowing better, to remove crossinline to only make the code compile

Tail cal optimization - What is the catch?

TCO exists already for a long time and it is used widely now in many programming languages. Some programming languages use it under the hood, and other programming languages, like Kotlin or Scala, use special keywords to signal to the compiler to perform TCO. We can describe TCO like this:

Since the late 50’s TCO was alreadya theory intentend to be applied toTail Recursivity. It allows tailrecursive functions to betransformed into iterative functionsin the compiled code for betterperformance

The idea is not only to improve performance of the code we are using, but most importantly, to avoid stackoverflow errors. To better understand this, let's have a look at the examples that I have placed in the module carparts-tailrec located at the root of this project:

sealed interface Part {
    val totalWeight: Double
}

sealed interface ComplexPart  : Part {
    val parts: List<Part>
}
data class CarPart(val name: String, val weight: Double) : Part {
    override val totalWeight: Double
        get() = weight
}

data class ComplexCarPart(
    val name: String,
    val weight: Double,
    override val parts: List<Part>
) :
    ComplexPart {
    override val totalWeight: Double
        get() = weight
}

data class Car(
    val name: String,
    override val parts: List<Part>
) : ComplexPart {
    override val totalWeight: Double
        get() = parts.sumOf { it.totalWeight }
}
Enter fullscreen mode Exit fullscreen mode

In this example I'm declaring a few data classes that I will use to create a kind of data tree set with different nodes where each nod has a weight corresponding to each car part. Each car can contain different lists of car parts that can be comples or simple. If they are comples, than that means that they are composed of may other parts with different weights having itself already some composition with a separate weight to support them. This is why we find here a Car, a ComplexCarPart, and a CarPart. With this, we can then create two cars in a list like this:

listOf(
    Car(
        "Anna", listOf(
            CarPart("Chassis", 50.0),
            CarPart("Engine", 100.0),
            CarPart("Transmission", 150.0),
            ComplexCarPart(
                "Frame", 500.0,
                listOf(
                    CarPart("Screw", 1.0),
                    CarPart("Screw", 2.0),
                    CarPart("Screw", 3.0),
                    CarPart("Screw", 4.0),
                )
            ),
            CarPart("Suspension", 200.0),
            CarPart("Wheels", 100.0),
            CarPart("Seats", 50.0),
            CarPart("Dashboard", 30.0),
            CarPart("Airbags", 20.0)
        )
    ),
    Car(
        "George", listOf(
            ComplexCarPart(
                "Chassis", 300.0,
                listOf(
                    CarPart("Screw", 1.0),
                    CarPart("Screw", 2.0),
                    CarPart("Screw", 3.0),
                    CarPart("Screw", 4.0),
                )
            ),
            CarPart("Engine", 300.0),
            CarPart("Transmission", 150.0),
            CarPart("Seats", 50.0),
            CarPart("Dashboard", 30.0),
            CarPart("Airbags", 20.0)
        )
    )
)
Enter fullscreen mode Exit fullscreen mode

In order to calculate the total weight of the car, I am using this function which looks good but it does have a story behind it than just the declaration of it:

tailrec fun totalWeight(parts: List<Part>, acc: Double = 0.0): Double {
    if (parts.isEmpty()) {
        return acc
    }
    val part = parts.first()
    val remainingParts = parts.drop(1)
    val currentWeight = acc + part.totalWeight
    return when (part) {
        is ComplexPart -> totalWeight(remainingParts + part.parts, currentWeight)
        else -> totalWeight(remainingParts, currentWeight)
    }
}
Enter fullscreen mode Exit fullscreen mode

Looking at totalWeight, we can see that all possible last calls to this function recursively are the call to the function itself. This is already enough, but a great tale sign of tail recursive functions is the fact that an accumulator, represented as acc in this case is passed on as a parameter for this function. This is the reason why this function is said to be tail recursive. The compiler will not make a build if we use another kind of recursive function. The keyword tailrec serves two purposed in this case. It tells the compiler that it should consider this function as a candidate for tail call optimization and it informs the developer during design time, if the function remains being a tail recursive function. It serves as a guide for both developers and the compiler, we could say.


A great question that was raised during my presentation was if the compiler can recognize tail recursive functions without using tailrec. The compiler can do that but it will not apply TCO to it unless we apply tailrec before the function declaration. If we do not apply tailrec, nothing unusual will happen:

public static final double totalWeight(@NotNull List parts, double acc) {
      Intrinsics.checkNotNullParameter(parts, "parts");
      if (parts.isEmpty()) {
         return acc;
      } else {
         Part part = (Part)CollectionsKt.first(parts);
         List remainingParts = CollectionsKt.drop((Iterable)parts, 1);
         double currentWeight = acc + part.getTotalWeight();
         return part instanceof ComplexPart ? totalWeight(CollectionsKt.plus((Collection)remainingParts, (Iterable)((ComplexPart)part).getParts()), currentWeight) : totalWeight(remainingParts, currentWeight);
      }
}
Enter fullscreen mode Exit fullscreen mode

Nothing special happened as expected, but with tailrec applied, then we get something very, very different:

public static final double totalWeight(@NotNull List parts, double acc) {
  Intrinsics.checkNotNullParameter(parts, "parts");

  while(!parts.isEmpty()) {
     Part part = (Part)CollectionsKt.first(parts);
     List remainingParts = CollectionsKt.drop((Iterable)parts, 1);
     double currentWeight = acc + part.getTotalWeight();
     if (part instanceof ComplexPart) {
        List var8 = CollectionsKt.plus((Collection)remainingParts, (Iterable)((ComplexPart)part).getParts());
        parts = var8;
        acc = currentWeight;
     } else {
        parts = remainingParts;
        acc = currentWeight;
     }
  }

  return acc;
}
Enter fullscreen mode Exit fullscreen mode

I have some slides at the bottom of this presentation and as I am writing this text, I noticed that even in this case, the transformation is slightly different than the one I show in the slides. I the slides, the example comes from decompiling the byte code in the same way as I do here, but in this particular case the compiler did something fundamentally different. In this case, the while loop keeps going until the parts List is empty, where as in the slides the generated example does an endless while loop until it returns when the parts list is empty. In any case, the principle is the same. Using tailrec our original tail-recursive function is transformed into an iterative function. What this does is only create one single stack where the whole process occurs. In this case we avoid making multiple recursive calls. But the big advantage is the fact that we will never get a StackOverflowException exception this way. We may get an OutOfMemoryExcpetion or any other related to resource exhaustion, but never really an overflow related error. Performance is a part of it purely because although time and space complexities for the tailrecursive function and the iterative function is mathematically the same, there is still an small overhead in generating the different call stack frames.


But we could have done this anyway in the code in Kotlin also and so why didn't we do that? The best answer to that is in my opinion that Kotlin relies at its core in immutable principles and null-safety. Also in general terms it is regarded as bad practice to reuse input parameters and change their values. Also, with a terrible reputation, is the usage of loops in the code. Coding using a tail recursive function leads to code that is very easy to understand and to follow its execution. That is what tailrec aims to provide. We can implement beautiful, but otherwise very poorly efficient and prone to StackOverflowException code, and still make it work as it should in the bytecode. Understanding tailrec, what it does and which problems it tries to solve is crucial when trying to understand our code and potentially finding the source of bugs.

Data classes and Frameworks - Why doesn't it work ... and why it does?

This is probably the most enigmatic issue of working with Data classes in different frameworks. Whether you have been working with the Spring Framework, Quarkus, or even the odd case with the old way of deploying applications using JEE war packages, you may have come across a common way of solving problems where the annotations don't seem to do anything by applying @field: as a prefix to you annotation of choice. Or maybe you have found that not even that works.

Kotlin provides use-site targets thatallow us to specify where particularannotations have to be applied.Sometimes we need them andsometimes we don’t

For this example I have created and example using the spring framework and it is located on the carpart-data-structure module. To run the example for this module we will need to start the docker containers using the docker-compose.yaml file at the root of the project. So let's first go there and run docker-compose up -d. After the service is running, let's have a look at the following entity:

@Table(name = "CAR_PARTS")
@Entity
data class CarPart(
    @Id
    val id: Long,
    @Column
    @field:NotNull
    @field:Size(min=3, max=20)
    val name: String,
    val productionDate: Instant,
    val expiryDate: Instant,
    val barCode: Long,
    @field:Min(value = 5)
    val cost: BigDecimal
)
Enter fullscreen mode Exit fullscreen mode

This entity will apply the validation correctly, but why doesn't it work exactly in the same way as if we remove the @field use-site target? Let's first have a look at what happens in this specific case. The decompiled bytecode looks like:

public final class CarPart {
   @Id
   private final long id;
   @Column
   @NotNull
   @Size(
      min = 3,
      max = 20
   )
   @org.jetbrains.annotations.NotNull
   private final String name;
   @org.jetbrains.annotations.NotNull
   private final Instant productionDate;
   @org.jetbrains.annotations.NotNull
   private final Instant expiryDate;
   private final long barCode;
   @Min(5L)
   @org.jetbrains.annotations.NotNull
   private final BigDecimal cost;
(...)
}
Enter fullscreen mode Exit fullscreen mode

We can see that the annotations have been applied correctly to the fields and this decompiled code gives us a guarantee that everything is working. The integration tests for this case should also work flawlessly and we could try the running application using the test-requests.http file that I have created for it:

###
POST http://localhost:8080/api/v1/carparts/create
Content-Type: application/json

{
  "id": 0,
  "name": "brakesbrakesbrakesbrakesbrakesbrakesbrakes",
  "productionDate": 1713787922,
  "expiryDate": 1713787922,
  "barCode": 12345,
  "cost": 1234
}
Enter fullscreen mode Exit fullscreen mode

By running this request, we should expect a validation error and if we perform this request we should be getting something like:

2024-04-26T14:01:49.818+02:00 ERROR 225329 --- [carparts-manager] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction] with root cause

jakarta.validation.ConstraintViolationException: Validation failed for classes [org.jesperancinha.talks.carparts.carpartsdatascructures.domain.CarPart] during persist time for groups [jakarta.validation.groups.Default, ]
List of constraint violations:[
        ConstraintViolationImpl{interpolatedMessage='size must be between 3 and 20', propertyPath=name, rootBeanClass=class org.jesperancinha.talks.carparts.carpartsdatascructures.domain.CarPart, messageTemplate='{jakarta.validation.constraints.Size.message}'}

Enter fullscreen mode Exit fullscreen mode

And this of course makes perfect sense. However if we remove @field and let Kotlin decide that for us, then, after running the application, we will get no error and the response will just be this one:

POST http://localhost:8080/api/v1/carparts/create

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 26 Apr 2024 12:04:12 GMT

{
  "id": 0,
  "name": "brakesbrakesbrakesbrakesbrakesbrakesbrakes",
  "productionDate": "2024-04-22T12:12:02Z",
  "expiryDate": "2024-04-22T12:12:02Z",
  "barCode": 12345,
  "cost": 1234
}
Response file saved.
> 2024-04-26T140412.200.json

Response code: 200; Time: 345ms (345 ms); Content length: 164 bytes (164 B)
Enter fullscreen mode Exit fullscreen mode

This just means that now the annotations do not work for an entity that has been declared like this one:

@Table(name = "CAR_PARTS")
@Entity
data class CarPart(
    @Id
    val id: Long,
    @Column
    @NotNull
    @Size(min=3, max=20)
    val name: String,
    val productionDate: Instant,
    val expiryDate: Instant,
    val barCode: Long,
    @Min(value = 5)
    val cost: BigDecimal
)
Enter fullscreen mode Exit fullscreen mode

So why doesn't the data gets validated in this last case? As Kotlin advances, one of the goals it, just like in many other programming languages is to reduce what we call boiler-plate code as much as possible. However, having said that, some thing stop working as they used to when we evolve in that direction. With the advent of data class and Java records, we also remove the places where Java developers have been used to placing annoations. Using this decorative style of programming used to be very easy to do because we would find the getter and setters, parameters and fields easy to see in the code. Kotlin data class cramps everything up into a single line per class member. By doing that, we need to tell Kotlin where it should apply the annotation simply because there is no visible way to do so. To data, we can use Kotlin use-site targets, which are the answer to that problem. And it is true that for most cases, the @field will solve these problems for us. But there is a reason for that the is frequently overlook. Kotlin has rules for that and we can read them in their website and they go like this:

If you don't specify a use-site target, the target is chosenaccording to the @Target annotation of the annotation beingused. If there are multiple applicable targets, the first applicabletarget from the following list is used:

  • param
  • property
  • field

And so, just to give an example if we look at property @Size and check what it says in its implementation we find this:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Size {
(...)
}
Enter fullscreen mode Exit fullscreen mode

It basically contains all possible targets and that means that we can apply this annotation anywhere. But there is a problem with this, if we follow the rules provided by Kotlin we will see that @Size will be applied in param and this is bad news for our code. To understand why, let's have a look at the decompiled code of the entity that doesn't validate anything anymore:

public CarPart(long id, 
               @jakarta.validation.constraints.NotNull 
               @Size(min = 3,max = 20) @NotNull String name,
               @NotNull Instant productionDate,
               @NotNull Instant expiryDate, long barCode, 
               @Min(5L) @NotNull BigDecimal cost) {
  Intrinsics.checkNotNullParameter(name, "name");
  Intrinsics.checkNotNullParameter(productionDate, "productionDate");
  Intrinsics.checkNotNullParameter(expiryDate, "expiryDate");
  Intrinsics.checkNotNullParameter(cost, "cost");
  super();
  this.id = id;
  this.name = name;
  this.productionDate = productionDate;
  this.expiryDate = expiryDate;
  this.barCode = barCode;
  this.cost = cost;
}
Enter fullscreen mode Exit fullscreen mode

And indeed, we now observe that @Size has been applied to a parameter. But now you may ask why this doesn't work? What is the difference between putting @Size in a parameters or in a field? The Spring Framework uses AOP (Aspect Oriented Programming) to be able to validate data either via entities or Dtos. However this comes out of the box to work very nicelly with fields but it is not ready to work with params. If we don't do something special, by default, applying the mentioned annotation to the contructor parameters will never take any effect.


Making sure we know how to use site targets in Kotlin is of crucial importance. I cannot stress enough the time that can be lost by trying to fix problems like this without knowing the rules that Kotlin establishes for us in order to be able to apply annotations correctly.

Delegates and otheruse-site targets - But how can we use it?

Delegates are something many of use already use without thinking about it in Kotlin. Namely use make a lot of use of by lazy which allow us to save some time during startup and only use resources when they are absolutely necessary. It can be overused as well, but this is just one example of it. However we do have at our disposal one particular use-site target that can be very intriguing.

Delegation is a great part of the Kotlin programming language and it is quite different than what we are used to seeing in Java

The use-site target I will discuss in this last segment is the @delegate use site-target. With it, we can apply a decoration or stereotype to a delegate and to see that working let's have a look at the following example locate in module carparts-use-site-targets at the root folder of the project:

interface Horn {
    fun beep()
}

class CarHorn : Horn {
    override fun beep() {
        println("beep!")
    }
}

class WagonHorn : Horn {
    override fun beep() {
        println("bwooooooo!")
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example I am only creating the return type Horn where I also declare two subclasses CarHorn and WagonHorn. To return values of these types I have then created a specific delegate for them with only a getValue in mind:

class SoundDelegate(private val initialHorn: Horn) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Horn {
        return initialHorn
    }
}
Enter fullscreen mode Exit fullscreen mode

This delegate will only return the value that is being set on its field on creation.

Finally I created two annotations that will do nothing specifically in this case, but they will allow us to see how this gets applied to a delegate:

annotation class DelegateToWagonHorn

annotation class DelegateToCarHorn
Enter fullscreen mode Exit fullscreen mode

And finally to demonstrate this, a code where we can see all of the above being applied:

class HornPack {
    @delegate:DelegateToWagonHorn
    val wagonHorn: Horn by SoundDelegate(CarHorn())

    @delegate:DelegateToCarHorn
    val carHorn: Horn by SoundDelegate(WagonHorn())
}
Enter fullscreen mode Exit fullscreen mode

If we decompile the bytecode from this into Java we will see something that is very obvious, but at the same time it is very important to draw some attention to it:

public final class HornPack {
   static final KProperty[] $$delegatedProperties;
   @DelegateToWagonHorn
   @NotNull
   private final SoundDelegate wagonHorn$delegate = new SoundDelegate((Horn)(new CarHorn()));
   @DelegateToCarHorn
   @NotNull
   private final SoundDelegate carHorn$delegate = new SoundDelegate((Horn)(new WagonHorn()));

   @NotNull
   public final Horn getWagonHorn() {
      return this.wagonHorn$delegate.getValue(this, $$delegatedProperties[0]);
   }

   @NotNull
   public final Horn getCarHorn() {
      return this.carHorn$delegate.getValue(this, $$delegatedProperties[1]);
   }

   static {
      KProperty[] var0 = new KProperty[]{Reflection.property1((PropertyReference1)(new PropertyReference1Impl(HornPack.class, "wagonHorn", "getWagonHorn()Lorg/jesperancinha/talks/carparts/Horn;", 0))), Reflection.property1((PropertyReference1)(new PropertyReference1Impl(HornPack.class, "carHorn", "getCarHorn()Lorg/jesperancinha/talks/carparts/Horn;", 0)))};
      $$delegatedProperties = var0;
   }
}
Enter fullscreen mode Exit fullscreen mode

There are two imporant aspects when having a look at this code. The resulting decompiled code, shows us that the annotations created have been applied to the delegates and if we look at another aspect of it, we can see that we have two more accessors made available for us to be able to access the two horns that we have created and these methods are: getWagonHorn and getCarHorn. The interesting about this bit is that it seems to suggest that we can apply a use-site target to an annotation that we want to apply to a delegate and maybe use a use-site target to an annotation that we want to apply to a getter of the property that we want to use in our code via the delegate. To test this I have created another example, which is located in a module we have already seen before in carparts-data-structures:

@Service
data class DelegationService(
    val id: UUID = UUID.randomUUID()
) {
    @delegate:LocalDateTimeValidatorConstraint
    @get: Past
    val currentDate: LocalDateTime by LocalDateTimeDelegate()
}
Enter fullscreen mode Exit fullscreen mode

In this case, the DelegationServer is composed of only one field where its assignment is being done via a delegate that we have create to be one that returns a LocalDateTime. The idea of injecting a service with an annotated delegate is to allow Spring to perform its operations and wrap this delegate in a CGLIB proxy. But before we continue, let's first have a look at the implementation of the LocalDateTimeValidatorConstraint, which I didn't had the time to explain what it does and its purpose during the presentation:

@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [LocalDateTimeValidator::class])
@MustBeDocumented
annotation class LocalDateTimeValidatorConstraint(
    val message: String = "Invalid value",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<*>> = []
)

class LocalDateTimeValidator : ConstraintValidator<LocalDateTimeValidatorConstraint, LocalDateTimeDelegate> {
    override fun initialize(constraintAnnotation: LocalDateTimeValidatorConstraint) {
    }

    override fun isValid(value: LocalDateTimeDelegate, context: ConstraintValidatorContext): Boolean {
        return when(Locale.getDefault().country ){
            "NL","US" -> true
            else -> false
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What this validator does is to invalidate our delegate if the default Local is anything other than "NL" or "US". To this, we can use the same test-requests.http file, but here we need to perform a request to another endpoint to test this:

###
POST http://localhost:8080/api/v1/carparts/create/extended
Content-Type: application/json

{
  "id": 0,
  "name": "brakes",
  "productionDate": 1713787922,
  "expiryDate": 1713787922,
  "barCode": 12345,
  "cost": 1234
}
Enter fullscreen mode Exit fullscreen mode

Before we make this request though, make sure that you have reverted the changes we made before to test our code. Once that is done, if we make this request we should get a normal response back and we should find something like this in the service logs:

2024-04-26T14:54:57.294+02:00  INFO 234172 --- [carparts-manager] [nio-8080-exec-1] o.j.t.c.c.converter.Converters           : Tried to convert data CarPart(id=0, name=brakes, productionDate=2024-04-22T12:12:02Z, expiryDate=2024-04-22T12:12:02Z, barCode=12345, cost=1234)
2024-04-26T14:54:57.325+02:00  INFO 234172 --- [carparts-manager] [nio-8080-exec-1] j.t.c.o.j.t.c.c.c.LocalDateTimeValidator : 2024-04-26T14:54:57.325284145

Enter fullscreen mode Exit fullscreen mode

We get this LocalDateTime time at the end of the logs. The implementation is easy to follow and looks like this:

@PostMapping("create/extended")
fun createCarPartExtended(
    @RequestBody @Valid carPartDto: CarPartDto,
    @Valid delegationService: DelegationService,
) = carPartServiceExtended.createCarPart(carPartDto)
    .also {
        logger.info("{}", delegationService.currentDate)
    }
Enter fullscreen mode Exit fullscreen mode

We are in this case using a carPartServiceExtended here and this is just a service that I am createing using a @get site target. It is only another instance of carPartService created using a delegate and declared as a bean like this:

@SpringBootApplication
class CarPartsDataStructureApplication(
    carPartDao: CarPartDao
) {
    @get:Bean("carPartServiceExtended")
    val carPartServiceExtended: CarPartsService by CarPartsService(carPartDao)
}
Enter fullscreen mode Exit fullscreen mode

But let's now focus at what happens to the DelegationService where we are using both use-site targets. The field currentDate is also annotated with @get: Past, which means that we should only accept LocalDateTime in the past. This means that depending on how your Locale is configured on your machine, this code may fail or not. LocalDateTime wise, it should always work because, no matter what, our LocalDateTime will always be in the past. But lets now change the code to make it impossible to get a positive validation. Let's change it to validate to Future:

@Service
data class DelegationService(
    val id: UUID = UUID.randomUUID()
) {
    @delegate:LocalDateTimeValidatorConstraint
    @get: Future
    val currentDate: LocalDateTime by LocalDateTimeDelegate()
}

Enter fullscreen mode Exit fullscreen mode

And make the delegation validation check for a non existing locale like for example CatLand:

class LocalDateTimeValidator : ConstraintValidator<LocalDateTimeValidatorConstraint, LocalDateTimeDelegate> {
    override fun initialize(constraintAnnotation: LocalDateTimeValidatorConstraint) {
    }

    override fun isValid(value: LocalDateTimeDelegate, context: ConstraintValidatorContext): Boolean {
        return when(Locale.getDefault().country ){
            "CatLand" -> true
            else -> false
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And a great question to be asked at this point even before we continue is how many validation fails could we find before making the request? Is it 0, 1 or 2? If we restart the code and run the same request we will get a curious error. To start out we a 400 error:

{
  "timestamp": "2024-04-26T13:08:04.411+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/api/v1/carparts/create/extended"
}
Enter fullscreen mode Exit fullscreen mode

And the service logs will corroborate this story, but they will also tell us the validation error that have occurred:

2024-04-26T15:08:04.405+02:00  WARN 237106 --- [carparts-manager] [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [1] in public org.jesperancinha.talks.carparts.carpartsdatascructures.dto.CarPartDto org.jesperancinha.talks.carparts.carpartsdatascructures.controller.CarPartsController.createCarPartExtended(org.jesperancinha.talks.carparts.carpartsdatascructures.dto.CarPartDto,org.jesperancinha.talks.carparts.org.jesperancinha.talks.carparts.carpartsdatascructures.service.DelegationService) with 2 errors: [Field error in object 'delegationService' on field 'currentDate$delegate': rejected value [org.jesperancinha.talks.carparts.LocalDateTimeDelegate@6653aa1c]; codes [LocalDateTimeValidatorConstraint.delegationService.currentDate$delegate,LocalDateTimeValidatorConstraint.currentDate$delegate,LocalDateTimeValidatorConstraint]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [delegationService.currentDate$delegate,currentDate$delegate]; arguments []; default message [currentDate$delegate]]; default message [Invalid value]] [Field error in object 'delegationService' on field 'currentDate': rejected value [2024-04-26T15:08:04.390037846]; codes [Future.delegationService.currentDate,Future.currentDate,Future.java.time.LocalDateTime,Future]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [delegationService.currentDate,currentDate]; arguments []; default message [currentDate]]; default message [must be a future date]] ]
Enter fullscreen mode Exit fullscreen mode

Two important error messages that we should have a good look at are these [Invalid value], which refers to the validation provided for the delegate itself and [must be a future date], which refers to the currentDate getValue of the delegate.

So let's now have a look at how this looks like in the decompiled code:

public class DelegationService {
   static final KProperty[] $$delegatedProperties;
   @NotNull
   private final UUID id;
   @LocalDateTimeValidatorConstraint
   @NotNull
   private final LocalDateTimeDelegate currentDate$delegate;
(...)
   @Past
   @NotNull
   public LocalDateTime getCurrentDate() {
      LocalDateTime var1 = this.currentDate$delegate.getValue(this, $$delegatedProperties[0]);
      Intrinsics.checkNotNullExpressionValue(var1, "getValue(...)");
      return var1;
   }
Enter fullscreen mode Exit fullscreen mode

We can clearly see in this case that @LocalDateTimeValidatorConstraint has been applied to the delegate and @Past has been applied to the getter of the property we want it to be applied to.


What is great about what we just saw in this case is that we now know and understand how this particular annotation @delegate can be used. I have, however not yet seen a use-case for this in server-side development using Spring. It could be that this has special use cases for Spring or perhaps in other frameworks. In spite of that it is good to have this present when developing in Kotlin.

Conclusion

My idea with this presentation was to promote a few of ideas:

  1. Better understanding of the Kotlin Language
  2. Don’t fight the Spring Framework or anything else like Quarkus. They are not evil and they are not magic.
  3. Read the Kotlin documentation and only use Google as a last resort.
  4. Nothing is perfect and Kotlin also falls into that category and recognizing that, allow us to be better

Finally I would like to say a massive thank you to the Kotlin Dutch User Group, shout out to @raphaeldelio for reaching out, Xebia NL and JetBrains for organizing this event. Thanks eveyone for coming to the event!

Further I'd like to mention that the presentations of Jolan Rensen and Jeroen Rosenberg were really worth watching. To the date of this publication I could only find Jolan's work on DataFrameAndNotebooksAmsterdam2024.

Finally I just want to say that I am inherently someone that looks at stuff with critical thinking in mind and a lot of curiosity. This means that it is because I like Kotlin so much that I pay attention to all the details surrounding the language. There are things that Kotlin offers that may offer different challenges and it may play out different in the future but in my opinion it is a great engineering invention for the software development world.

Source code and slides

GitHub logo jesperancinha / kotlin-mysteries

Discover 5 oddities about Kotlin that can occur in the workplace, your personal projects or that you may have come across in the community.

Kotlin Mysteries

Introduction

This repo is a list of examples in Kotlin that escape the norm. This is your guide to the mysterious and different in Kotlin.

Topics

  1. Nullability
    1. Working with the Spring Framework - carparts-manager
    2. Reflection to force nulls - carparts-null-play
  2. inline and cross-inline - carparts-inline
    1. The Java overview
  3. Tail recursive => Tail Cal Optimization (TCO) - carparts-tairec
    1. What is it
    2. Why?
    3. How it makes us work recursively and not use mutable
  4. Data classes - carparts-data-structures
    1. Why things work and why things don't work
    2. How to fix the ones that don't
    3. How to work with use-site targets.
  5. What does a delegate do? and other use-site targets.
    1. Delegates - carparts-use-site-targets
    2. Delegates with Spring - carparts-data-structures

About me

GitHub followers






Decoding Kotlin - Your Guide To Solving The Mysterious in Kotlin | PDF | Computer Programming | Software Engineering

These are the slides of the presentation I gave at the JetBrains HQ by the RAI in Amsterdam named "Decoding Kotlin - Your Guide to Solving the Mysterious in Kotlin". I want to express a big thank you to Xebia and JetBrains for the opportunity. I gave this presentation on the 24th of April 2024.

favicon scribd.com

Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx

Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx - Download as a PDF or view online for free

favicon slideshare.net

If you are interest in know more about TailRec you may also find interesting a video I made about it right over here:

And if you just want a quick, easy listening way of learning about the data classes case I mention in this presentation I also have a video that talks about that over here:

At the end of this presentation I mention a custom validation using Spring. If you know the AssertTrue and AssertFalse annotations you may ask why I didn't use those. That is because both only validate for boolean returned values. I have made videos about both, but for this presentation this the one of interest where I explain how to make custom validations:

About me

Further reading

Top comments (0)