Skip to content

Instantly share code, notes, and snippets.

@nimaiwalsh
Last active August 8, 2024 23:07
Show Gist options
  • Save nimaiwalsh/c6d09b95763cd44e2af4e077c8d53086 to your computer and use it in GitHub Desktop.
Save nimaiwalsh/c6d09b95763cd44e2af4e077c8d53086 to your computer and use it in GitHub Desktop.
kotlin-notes

Notes from Effective Kotlin: Best Practice

Safety

Limit mutability

The general rule is that one should not create unnecessary ways to mutate a state. Every way to mutate a state is a cost. Every mutation point needs to be understood and maintained. We prefer to limit mutability. General rules:

  • Prefer val over var.
  • Prefer an immutable property over a mutable one
  • Prefer objects and classes that are immutable over mutable ones
  • If you need immutable objects to change, consider making them data classes and using copy.
  • When you hold a state, prefer read-only over mutable collections. e.g var list: listOf over val mutableList: mutableListOf().
  • Design your mutation points wisely and do not produce unnecessary ones.
  • Do not expose mutable objects.

Minimise the scope of variables

  • For many reasons, we should prefer to define variables for the closest possible scope. Also, we should prefer val over var for local variables, and we should always be aware of the fact that variables are captured in lambdas.

Minimise platform types as soon as possibe

  • Platform type - a type that comes from another language and has unknown nullability. e.g java String is always nullable unless annotated with @NotNull

Do not expose inferred types

  • The general rule is that if we are not sure about the type, we should specify it because this is important information that should not be hidden. Additionally, for the sake of safety, in an external API we should always specify types. We cannot let them be changed by accident. Inferred types can be too restrictive or can too easily change when a project evolves.

Specify your expectations for arguments and states

  • require block - a universal way to specify expectations for arguments.
  • check block - a universal way to specify expectations for states.
  • assert block - a universal way to check if something is true. Such checks in the JVM are evaluated only in the testing mode.
  • The Elvis operator with return or throw.

Prefer standard errors to custom ones.

  • prefer the standrad exceptions if they exiest instead of creating custom ones. Developers are aware of them.

Prefer a nullable or Result result type when the lack of a result is possible

  • We should prefer to return null or Result.failure when an error is expected, and we should throw an exception when an error is not expected.

Handle nulls property

3 ways to handle nulls

  • Handle nullability safely using a safe call ?., smart casting, the Elvis operator, etc.
  • Throw an error
  • Refactor this function or property so that it won’t be nullable

Close resource with use

Operate on objects that implement Closeable (e.g InputStream) or AutoCloseable using use. This is a safe and easy option. When you need to operate on a file, consider useLines, which produces a sequence to iterate over the next lines.

As this support is often needed for files and it is common to read files line by line, there is also a useLines function in the Kotlin Standard Library that gives us a sequence of lines (String) and closes the underlying reader once the processing is complete.

Readability

An operator’s meaning should be consistent with its function name

Use operator overloading conscientiously. A function’s name should always be coherent with its behavior. Avoid cases where operator meaning is unclear. Clarify it by using a regular function with a descriptive name instead. If you wish to have a more operator-like syntax, then use the infix modifier or a top-level function.

Example

operator fun Int.times(operation: () -> Unit) {
  repeat(this) { operation() }
}

infix fun Int.timesRepeated(operation: () -> Unit) {
  repeat(this) { operation() }
}

Code re-use

Consider using variance for generic types

  • The default variance behavior of a type parameter is invariance. If, in Cup<T>, type parameter T is invariant and A is a subtype of B, then there is no relation between Cup<A> and Cup<B>.
  • The out modifier makes a type parameter covariant. If, in Cup<T>, type parameter T is covariant and A is a subtype of B, then Cup<A> is a subtype of Cup<B>. Covariant types can be used at out-positions.
class Cup<out T>
open class Dog
class Puppy: Dog()
 
fun main(args: Array<String>) {
  val b: Cup<Dog> = Cup<Puppy>() // OK
  val a: Cup<Puppy> = Cup<Dog>() // Error
 
  val anys: Cup<Any> = Cup<Int>() // OK
  val nothings: Cup<Nothing> = Cup<Int>() // Error
}
  • in makes a type parameter contravariant. If, in Cup<T>, type parameter T is contravariant and A is a subtype of B, then Cup<B> is a subtype of Cup<A>. Contravariant types can be used at in-positions.
class Cup<in T>
open class Dog
class Puppy: Dog()
 
fun main(args: Array<String>) {
  val b: Cup<Dog> = Cup<Puppy>() // Error
  val a: Cup<Puppy> = Cup<Dog>() // Ok
 
  val anys: Cup<Any> = Cup<Int>() // Error
  val nothings: Cup<Nothing> = Cup<Int>() //  Ok
}

Adbstractiom Design

API Stability

  • Use Semantic Versioning (SemVer): in this system, we compose the version number from 3 parts: MAJOR.MINOR.PATCH. Each of those parts is a positive integer starting from 0, and we increment each of them when changes in the public API have concrete importance. So we increment:

    • MAJOR version when you make incompatible API changes.
    • MINOR version when you add functionality in a backward-compatible manner.
    • PATCH version when you make backward-compatible bug fixes.
  • When we increment MAJOR, we set MINOR and PATCH to 0. When we increment MINOR we set PATCH to 0. Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. Major version zero (0.y.z) is for initial development; with this version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment