Kalan's Blog

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

Software Engineer / Taiwanese / Life in Fukuoka
This blog supports RSS feed (all content), you can click RSS icon or setup through third-party service. If there are special styles such as code syntax in the technical article, it is still recommended to browse to the original website for the best experience.

Current Theme light

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

Please notice that currenly most of posts are translated by AI automatically and might contain lots of confusion. I'll gradually translate the post ASAP

Learn from Kotlin: Kotlin DSL and Annotation

Recently, I developed a small tool for managing daily tasks using Kotlin. The main purpose was to involve the Backend colleagues in maintaining it together. Additionally, learning a new language always brings new inspiration and ideas, and there are many Java and JVM experts in the company who can provide valuable insights.

The functionality of this daily task tool is quite simple:

  • CRUD operations for TODO items. Each Todo item is scheduled, and if not completed within the specified time, a notification is sent to Slack.
  • Ability to create recurring schedules, such as daily checks for Pull Requests or Jira progress.

Writing Kotlin code is comfortable as it retains the benefits of static typing and compiler checks, while reducing the verbosity compared to Java. Kotlin also provides many useful syntax features to simplify code. This article will introduce some concepts I learned in Kotlin.

Kotlin DSL

The following code is a valid Kotlin syntax.

Bot.sendMessage(channelId) {
   section {
       plainText("This is a plain text section block.")
   } 
   section {
       markdownText("This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and <https://google.com|this is a link>")
   }
   divider()
}  

After executing the code, it will be transformed into the following JSON structure:

{
	"blocks": [
		{
			"type": "section",
			"text": {
				"type": "plain_text",
				"text": "This is a plain text section block.",
				"emoji": true
			}
		},
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and <https://google.com|this is a link>"
			}
		}
	]
}

You can combine and compose messages by defining the format specified within the Slack block.

Suppose you want to send a message to Slack. In Slack, you can use Block Kit to create messages. In the provided official API, you can write code like this:

val slack = Slack.getInstance()
val token = System.getenv("token")
val response = slack.methods(token).chatPostMessage { req -> req
  .channel("C1234567")
  .blocks {
    section {
      // "text" fields can be constructed via `plainText()` and `markdownText()`
      markdownText("*Please select a restaurant:*")
    }
    divider()
    actions {
      button {
          text("Farmhouse", emoji = true)
          value("v1")
      }
      button {
          text("Kin Khao", emoji = true)
          value("v2")
      }
    }
  }
}

It appears clear and concise, resembling the process of writing a certain Component, which instantly gives me a favorable impression of Kotlin's syntax.

In the exposed library, you can write your SQL queries like this:

val status = "DONE"
Todos.select {
    Todos.status eq (status)
}

This achieves a feeling of "writing SQL" using the features of Kotlin DSL.

So, how does Kotlin DSL work? It mainly involves several concepts:

  • Extension functions
  • Infix notation
  • Lambda expressions
  • Function literals with receiver

Extension Functions

In Kotlin, you can add a new method to a class without the need for inheritance or using decorators, using syntax like the following:

fun Int.add2() {
  this + 2
}

This allows you to directly call it within the Int type like this:

fun add() {
  val a = 100
  a.add2()
}

The implementation of an extension function does not directly insert the method into the class. Instead, when calling a member function with a dot, you can also call the method implemented by the extension. Why not implement it directly inside the class? Sometimes it may be due to the implementation of third-party libraries or the inability to easily modify the class, making extension functions a useful approach.

Infix Notation

In Kotlin, if you declare a function as infix, it means that this function can be called without using a dot or parentheses. To achieve the effect of omitting the dot and parentheses, infix notation must meet the following conditions:

  • It must be a member function or an extension function.
  • It must have only one parameter.
  • It cannot have default values.

Here is an example:

infix fun Int.addSome(num: Int) {
  this + num
}

fun add() {
  val a = 100
  a.addSome(10)
  a addSome 10
}

In this case, a.addSome(10) and a addSome 10 are completely equivalent. 10 addSome a is also a valid expression.

One of the most common examples of infix notation in Kotlin is the to infix function. In Kotlin, you can declare a map like this:

val map = mapOf("name" to "kalan", "age" to 25)

The to function is actually an infix function, which is a convenient syntax sugar for generating a Pair. Its implementation is as follows:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

Through this definition, we can reduce the visual noise in code and improve readability.

Lambda Expressions

Lambda expressions in Kotlin are highly flexible. You can write them like this:

fun test(a: Int, block: () -> Unit) {

}

test(1, {
  println("hello")
})

When the lambda expression is the last parameter, you can move it outside the parentheses, like this:

fun test(a: Int, block: () -> Unit) {

}

test(1) {
  println("hello")
}

Both forms are completely equivalent. Additionally, Kotlin performs type inference for lambda functions, so if you write it like this:

fun test(a: Int, block: (i: Int) -> Unit) {

}

test(1) {
  println("hello")
}

The IDE will display it as the value parameter. This it represents the value passed into the block. It is equivalent to:

fun test(a: Int, block: (i: Int) -> Unit) {

}

test(1) { i ->
  println("hello", i)
}

You can refer to Kotlin's Scope Functions to understand how they work.

Function Literals with Receiver

Here, we'll directly introduce an example from the official documentation. In Kotlin, you can write functions like this:

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

Let's focus on the init parameter, which is declared as a function with the type HTML.() -> Unit. This is called a function type with receiver. It means that at some point, we need to pass html as the context to the init function, somewhat similar to apply(context) in JavaScript, but with the advantage of being more type-safe. T.() -> K represents executing member functions of type T and returning K.

Next, let's look at another example. In kotlinx.serialization, we can declare a Json like this:

val serializer = Json {
  ignoreUnknownKeys = true
  encodeDefaults = true
  coerceInputValues = true
}

The underlying implementation is as follows:

public fun Json(from: Json = Json.Default, builderAction: JsonBuilder.() -> Unit): Json {
    val builder = JsonBuilder(from.configuration)
    builder.builderAction()
    val conf = builder.build()
    return JsonImpl(conf)
}

In the previous code, the actual execution would be like this:

val builder = JsonBuilder()
builder.ignoreUnkownKeys = true
builder.encodeDefaults = true
builder.coerceInputValues = true
builder.build()

By using these Kotlin features and leveraging imagination, you can create a simple yet powerful DSL syntax to simplify your code, while ensuring type safety.

Annotation

When implementing a Cronjob, I created a cronjob annotation to enhance readability, inspired by the @Scheduled annotation in Spring Boot:

@CronSchedule("00 18 * * 1-5")
class NotifyMeCheck: Scheduler(), Workable {
    override fun work(args: Arg) {
		// job implementation
    }
}

@Target(AnnotationTarget.CLASS)
@MustBeDocumented
annotation class CronSchedule(val cronExpression: String) {}

During runtime, I search for all classes marked with CronWorkEntry and having the CronSchedule annotation, and schedule the cronjob. It looks something like this:

CronWorkEntry::class.nestedClasses.forEach {
    val annotation = it.findAnnotation<CronSchedule>()
    if (annotation != null) {
      val worker = it.createInstance() as Scheduler
      worker.performCron(annotation.cronExpression,"${CronWorkEntry::class.qualifiedName!!}$${it.simpleName!!}", "", true)
    }
}

However, it.createInstance() as Scheduler feels a bit strange because there is no compile-time enforcement that the class must be a Scheduler. Therefore, if it does not inherit from Scheduler, an exception will be thrown. I wonder if there is a way to restrict the annotation to only apply to certain class types.

Prev

Svelte Summit 2020 notes and experience

Next

The strange phenomenon of the great god

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee