#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.
this is the part where kotlin-features are discussed and how to use them. Each topic consists of three parts:
- An explanation of the topic
- The rationale the topic
- Setup, which will be used in the do's and don'ts
- The do's and don'ts
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.
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)
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
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
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.
In this block the usage of apply, also, let and run are discussed.
with
is not discussed, as this is a redundant function.
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 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
.
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 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
andthis.run
are just evaluated as calls without usingapply
andrun
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!!!
(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
}
}
(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
}
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
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
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`
}
//DO
return when{
i < 2->...
i > 5 -> ...
else-> ...
}
//NOT ALLOWED
if(i<2){
...
} else if(i > 5) {
...
} else {
...
}
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")
}