Skip to content

Instantly share code, notes, and snippets.

@tieskedh
Last active October 11, 2017 11:10
Show Gist options
  • Save tieskedh/65a90173c867aca35e2b988ff4baabd1 to your computer and use it in GitHub Desktop.
Save tieskedh/65a90173c867aca35e2b988ff4baabd1 to your computer and use it in GitHub Desktop.
kotlin-styleguide

#Styleguide This is the style-guide of tornadoFX. This styleguide is different from normal styleguides in that it doesn't say where you should put the braces and where ou should put four spaces. This guide is used to talk about the things that really matter, like when to use which Kotlin-features and when to do what in the framework-implementation.

This guide has two segments. One segment which talks about the Kotlin-features and what to use. The other part is about when to do what in the framework.


Kotlin


this is the part where kotlin-features are discussed and how to use them. Each topic consists of three parts:

  1. An explanation of the topic
  2. The rationale the topic
  3. Setup, which will be used in the do's and don'ts
  4. The do's and don'ts

basic functions

In kotlin functions can be oneliners:

fun test() = "test"

This brings some freedom where you can take advantage of. To regulate this, there are some rules about the functions.

SimChain: use no returntype (unless required) for a chain of similar functions.

Sometimes, you have one implemented functions and multiple functions with different parameters, which call that base-function (see do and don't). Then call the base-function as simple as possible: without specifying return-type. In that case the return-type of all the functions change by changing only the base-function.

//DO
fun sayHi(name: String) = "hi $name"
fun sayHi(person: Person) = sayHi(person.name)
fun sayHi(group: Group) = sayHi(group.groupName)
//NOT ALLOWED
fun sayBye(name: String) = "bye $name"
fun sayBye(person: Person) : String = saybye(person.name)
fun sayBye(group: Group) : String = sayBye(group.groupName)

cleanFunction: If a function does not return Unit, don't use a body if possible

return is often not needed. By leaving it out, a function stays small. Add returntype if it is not immediately clear and the function is not part of a SimChain

fun do1() = "test" //do
fun do2() : String = ... //do

fun dont() : String{ return "test" } //dont

unitFunction: If a function returns Unit, always use a body

If the function is not part of a simChain, use a body when you return unit. This makes the function independent of returntype from other functions. Also, don't specify the returntype. This can easily be checked, by searching : Unit, when there are results, you violate this rule.

fun do1() { ... } //do

fun dont1() : Unit = println("hi") //NOT ALLOWED
// return-unit not in block
fun dont2() : Unit = dont1() //NOT ALLOWED
// return-unit not in block

operator overloading

Kotlin has a lot of rules about operator overloading. See their guide. We stick to these guide and never add something not corresponding to these rules. If something can be replaced by an operator, replace it with an operator. The only exception to this rule is when you want to check if a key exists in a map. Then you should just use the containsKey-function.

apply, also, let, run

In this block the usage of apply, also, let and run are discussed. with is not discussed, as this is a redundant function.

Explanation

function this/it returnValue
apply this receiver
also it receiver
run this block
let it block

the table above corresponds with the code below. If the returnvalue is block, the last line of the lambda is the returnvalue, otherwise the receiver will be returned. The this/it refers to how to call the receiver inside the block.

val returnValue = receiver.function{ this/it }

!! IMPORTANT: parameters overwrite the properties/functions of the apply:

class Test(var name: String)
fun main(vararg args: String) = testMe("method")

fun testMe(name: String){
    Test("class").apply{
        println(name) //prints "method"
        println(this.name) //prints "class"
    }
}

The rationale

The goal of also and apply is to mutate the receiver. The goal of let/run is to return a value based on the fact if the receiver is null or not null.

For helping to stick these functions to their right, is one simple rule for the usage of let/run: the meaning of the lambda can't change if the lambda will start with the keyword return.

The reason for this rule is that let/run has an implicit return. This means that when the code becomes too big, it can be unclear that the last expression will be returned.

When you can use both apply and run, you should choose apply. When you can use both also and let, you should choose also.

The reason why is that you probably don't care about the returnvalue or that the returntype is the receiver. In these cases, you are mutating the value, which is the task of also and apply.

======================================

The code used in the do's and dont's

class Outer(var outVal: String) : IOuter{
    fun outFun1() = "out1"
    fun outFun2() = "out2"
}
interface IReceiver
class Receiver(var recVal: String) : IReceiver{
    fun recFun1() = "rec1"
    fun recFun2() = "rec2"
}

The do's and don'ts

The controll of moving the this in Kotlin is a great addition. It can however quickly lead to code that is difficult to read. Therefore, there are strict rules on when to use apply and run. It's better to use an also or let too much (although this is irritating ;-) ) then an apply/run too little.

When you can use both apply and run choose for apply. When you can use both also and let use also.

IMPORTANT: When you need to call apply/run/ also/let is not documented ( yet) because this is something you should (learn to) feel.

IMPORTANT:

  • for the rules, this.apply and this.run are just evaluated as calls without using apply and run

the use of let/run

When (inside of a function) multiple statements must be executed and the statements must be passed as a block and don't return anything, use the run-call. ( you know or will know when this is the case when ou need it).

In the rest of the code, you use let\ run in combination with nulls. Inside the blocks, there must be as less ass possible (because you have an implicit return). when you don't call the let and run on an object, use run. When you do call it on an object, the rules are the same as when to choose also and apply.

@todo ADD EXAMPLE!!!

use apply/run when you use only functions and variables of the receiver

(without this rule, it is not clear which functions or which properties are being used (where do they come from?))

fun Outer.do(receiver: Receiver) {
    receiver.apply{
         recFun1()
         recFun2()
    }
}

fun Outer.dont(receiver: Receiver){
    receiver.apply{
        recVal = outFun1() // NOT ALLOWED!!! outFun1() is not from the receiver
        outVal = recFun2() //NOT ALLOWED!!! outVal is not from the receiver
    }
}

use apply when you use functions and variables of the receiver and with parameters which: are properties/are (extension) lambda's which only access receiver

(functions and parameters overwrite the class-functions and class-properties, so if the function is short enough, it is clear what is referred to)

fun Outer.do(
    receiver: Receiver, 
    var parVar1: MutableList<String>, 
    parVar2: (String)-> Unit,
    parVar3: Receiver.()->Unit,
    parVar4: ()->String) {
    outFun1()
    receiver.apply{
         recVal = parVar1[0]
         parVar2(parVar4()) //If you do this, you need a good reason for it
         parVar3()
    }
}

fun Outer.dont(
    receiver: Receiver, 
    var parVar1: MutableList<String>, 
    parVar2: Outer.()-> Unit,
    parVar3: Receiver.(Outer)->Unit) {
    receiver.apply{
         outVal = parVar1[0] // NOT ALLOWED outval is not a part of receiver 
         this@outer.parVar2() //NOT ALLOWED parVar2 depends on something of outer
         parVar3(this@outer) //NOT ALLOWED parVar3 depends on something of outer
    }
}

when using nested let/also only the most inner let/also doesn't have to be named

(it is more unreadable than a proper name. So the source of the it must be easily tracked down. In most of the cases, the most inner lambda is very small. This is why the it is easy to trace. When using a bigger inner lambda, then you should consider renaming the parameter in the inner-lambda as well)

fun Outer.do() {
    recVal?.let{recVal-> recVal.let{it} }

    recVal?.let{ recVal-> recFun1().let{ val1-> } }
}

fun Outer.dont() {
    recVal?.let{ it?.also{name->} //NOT ALLOWED 
    //let/also other than most inner is unnamed
}    
    recVal?.let{ it?.also{it->} //NOT ALLOWED 
    //naming something "it" is not naming something
}

when an unchainable function returns this it must use apply and add only returntype when required

as noted, this call doesn't count as a call to apply Most of the time, when we want to know which type a function returns, we look at the first line. Using the apply keyword here does not only shows what type is returned, but the object itself as well. Because you know what is returned, you know the type as well. This is why the type should be omitted. of course, when you want to return the type by it's interface or superclass, the returntype is required.

fun Outer.do1() = apply{}
fun Outer.do2() = this.apply{}
fun Outer.do3() : IOuter = apply{}

fun Outer.dont1() { return this } //NOT ALLOWED
// function returning itself must must use apply

fun Outer.dont2() : Outer = apply{} // NOT ALLOWED
// returntype is not needed 

when an unchainable function returns a parameter, use also/apply and add only returnType when needed

explanation? see above

fun Outer.do1(var parVal:String) = parVal.apply{}
fun Outer.do2(var parVal:String) = parVal.apply{}
fun Outer.do3(var parVal:String) : String? = parVal.apply{}

fun Outer.dont1(var parVal:String) { return parVal } // NOT ALLOWED
// function returning itself must must use apply

fun Outer.dont2(var parVal:String) : String = parVal.apply{} // NOT ALLOWED 
// returntype is not needed

never use let/run as a function-block

let and run have an implicit returntype. This makes reading the method more difficult. Therefor, choose explicit variants.

fun Outer.do1(var converter: Outer.()->String) = apply{
    outFun1()
    outFun2()
}.converter()

fun Outer.do2(var rec1: Receiver, var rec2: Receiver, var converter: Receiver.()->String) : String {
    rec1.recFun1()
    rec1.recFun2()
    return rec2.converted()
}

fun Outer.dont1(var converter: Outer.()->String) = let{
    outFun1()
    outFun2()
    converter()
} // NOT ALLOWED, let/run is not allowed as block-body

fun Outer.do2(var rec1: Receiver, var rec2: Receiver, var converter: Receiver.()->String) : String = rec1.run {
    recFun1()
    recFun2()
    rec2.converted()
} // NOT ALLOWED, let/run is not allowed as block-body

when

When when is used for when, then when makes when clearer than when if/else is used for when. When you write a when with more than two elses, you must use when for when. When when has less than 3 cases, then choose when if the cases are long. else choose for if/else.

return when{
    cases > 3                       ->`when`
    cases.length > long             -> `when`
    allCovered(enums/sealedClasses) -> `when`
    else                            -> `if`/`else`
}

elses: when you use multiple elses, use when

//DO  
return when{
    i < 2->...
    i > 5 -> ...
    else-> ...
}

//NOT ALLOWED
if(i<2){
   ...
} else if(i > 5) {
   ...
} else {
   ...
}

safeCast

The safeCast in Kotlin does something like:

inline infix fun <S> Any?.as?() : S? 
        = if(this is S) this else null

This can help in cases where you want to do something when it is of a specific type. When you want to check if the class is of some type, don't use multiple safeCasts, but go for a when. for the other parts use the safeCast

!!IMPORTANT: Don't use safeCast when you get an unchecked_cast warning! the safeCast is not safe in those casts!

fun do1(map: Map<*,*>, key: String) 
        = (map as? Map<String, Any?>)?.get(key)
fun do2(param: Param<*>) {
    (param as? Param<String>)?.let{ println("String") } 
             ?: println("other")
}

fun dont1(map: Map<*,*>, key: String) 
        = if(map is Map<String, Any?>) map[key] else null 
fun dont2(param: Param<*>) {
    if(param is Param<String>) println("String")
    else println("other")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment