Functions
A function gives a name to a piece of work. It can receive values as parameters and return a result.
fun finalPrice(price: Double, discountPercent: Int): Double {
val discount = price * discountPercent / 100
return price - discount
}
price and discountPercent are inputs. Double after the parentheses is the return type.
Expression-body functions
When a function contains one expression, use =:
fun double(value: Int): Int = value * 2
Kotlin can infer the return type here, but an explicit return type is often clearer for public APIs.
Default and named arguments
fun greeting(name: String, prefix: String = "Hello"): String =
"$prefix, $name"
greeting("Rakesh")
greeting(name = "Rakesh", prefix = "Welcome")
Default values reduce overloads. Named arguments explain what each value means and let callers change the order.
Practical scenario: an optional greeting prefix
fun greeting(name: String, prefix: String = "Hello"): String =
"$prefix, $name"
Execution:
greeting("Rakesh")does not provideprefix, so Kotlin uses"Hello".greeting(name = "Rakesh", prefix = "Welcome")supplies both values explicitly.- Named arguments make two adjacent strings easier to understand at the call site.
Hello, Rakesh, followed by Welcome, Rakesh.Java does not understand Kotlin default arguments in the same way. For a Java-facing API, @JvmOverloads can generate overloads, but use it deliberately because every overload becomes part of the public API.
Unit versus Nothing
The names are confusing at first because neither is used to return useful data. The key difference is whether control comes back to the caller:
| Question | Unit | Nothing |
|---|---|---|
| Does the function finish normally? | Yes | No |
| Does the caller continue to its next line? | Yes | Not normally |
| How many values exist for the type? | One: Unit | None |
| Typical use | Actions and callbacks | Throwing or never-ending paths |
Unit: the action finishes
fun showMessage(message: String): Unit {
println(message)
}
showMessage("Saved")
println("Continue")
Both lines run. showMessage completes and returns control to the caller. Kotlin technically returns the single Unit value, but the caller receives no useful information. You normally omit : Unit:
fun showMessage(message: String) {
println(message)
}
Use Unit for actions such as logging, updating state, saving data, sending analytics, and UI callbacks like () -> Unit.
Nothing: the path cannot continue normally
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
fun runForever(): Nothing {
while (true) {
println("Running")
}
}
fail throws. runForever never reaches its end. Neither function can return control normally, and there is no possible value of type Nothing.
Could these functions be declared as Unit?
Yes. This compiles and behaves the same at runtime:
fun failWithUnit(message: String): Unit {
throw IllegalStateException(message)
}
But the signature is less accurate. It says that normal completion is allowed, even though this implementation always throws. Nothing gives the compiler and callers the stronger guarantee that execution cannot continue through this path.
Why the stronger type matters
✕ Declared as Unit
fun failWithUnit(message: String): Unit {
throw IllegalStateException(message)
}
val name: String? = null
// Does not compile: the right side is Unit
// val result: String = name ?: failWithUnit("Missing name")
✓ Declared as Nothing
fun failWithNothing(message: String): Nothing {
throw IllegalStateException(message)
}
val name: String? = "Rakesh"
val result: String = name ?: failWithNothing("Missing name")
Kotlin reasons as follows:
- If
nameis non-null, the Elvis expression produces aString. - If
nameis null,failWithNothingthrows and that path stops. - Every path that continues therefore has a non-null
String.
Saved, The caller continued after Unit, and result = Rakesh.Android rule
✕ Do not crash for a normal failure
fun loadUser(): Nothing = error("Network failed")
✓ Model a recoverable failure
sealed interface LoadResult {
data class Success(val user: User) : LoadResult
data class Failure(val message: String) : LoadResult
}
Use Nothing for a truly impossible internal state or a helper that deliberately throws. Network errors, invalid public deep links, missing permissions, and empty search results are expected situations; represent them as data the UI can handle.
Unit means a function completes normally without returning useful information. Nothing means it cannot return normally, usually because it throws or never terminates. A throwing function can be declared as Unit, but Nothing is more precise and helps Kotlin reason about expressions such as nullableValue ?: error("Missing").
Local functions
A function can live inside another function when it is useful only there:
fun validateRegistration(name: String, email: String): Boolean {
fun isPresent(value: String) = value.isNotBlank()
return isPresent(name) && isPresent(email) && "@" in email
}
This keeps a helper close to its use and prevents unrelated code from calling it.
Step by step for validateRegistration("Rakesh", "rakesh@example.com"):
- The local
isPresentfunction is created for use only insidevalidateRegistration. - Both
nameandemailmust contain non-whitespace text. - The email must contain
@. - All three Boolean conditions must be true.
true, false, and false.Extension functions
An extension function lets you call a helper as if it belonged to a type:
fun String.initials(): String =
trim()
.split(Regex("\\s+"))
.filter { it.isNotEmpty() }
.joinToString("") { word -> word.first().uppercase() }
For the input " Rakesh Lakshmipathy ", the chain runs from top to bottom:
trim()removes spaces at the beginning and end →"Rakesh Lakshmipathy".split(Regex("\\s+"))splits on one or more whitespace characters →["Rakesh", "Lakshmipathy"].filter { it.isNotEmpty() }keeps only non-empty words. Here the list remains unchanged.joinToString("") { word -> ... }takes the first letter of each word, converts it to uppercase, and joins the letters without a separator →"RL".
The regex \\s+ means “one or more whitespace characters.” Kotlin needs two backslashes in the string so that the regular expression receives one backslash.
RL, then AL, followed by an empty line for the blank input.An extension does not modify the class. It is a normal function selected at compile time. It also cannot access private members of the class.
Overloading
Functions may share a name when their parameter lists differ:
fun format(value: Int) = "Number: $value"
fun format(value: Boolean) = if (value) "Yes" else "No"
Do not overload functions when the meaning changes drastically. A clear name is better than making callers guess which behavior an overload provides.
Open in Kotlin PlaygroundNumber: 42, followed by Yes. Kotlin selects the overload using the argument's compile-time type.Common mistakes
1. A dangerous default hides a required decision
✕ Avoid
fun deleteAccount(userId: Long, permanently: Boolean = true) { /* ... */ }
deleteAccount(42) // Permanent deletion happened without saying so
✓ Prefer
fun deleteAccount(userId: Long, permanently: Boolean) { /* ... */ }
deleteAccount(userId = 42, permanently = false)
Defaults are useful for harmless convenience. Do not default a security, payment, deletion, or data-loss decision.
2. A nullable result does not explain why no value exists
✕ Avoid
fun register(email: String): User? = null
The caller cannot tell whether the email was invalid, already registered, or the server failed.
✓ Prefer
sealed interface RegistrationResult {
data class Success(val user: User) : RegistrationResult
data object InvalidEmail : RegistrationResult
data object AlreadyRegistered : RegistrationResult
data class Failed(val cause: Throwable) : RegistrationResult
}
Use null when simple absence is the complete meaning. Use a result type when the caller must react differently to several outcomes.
3. A broad extension looks available everywhere
✕ Avoid
fun String.saveUserToDatabase() { /* hidden database dependency */ }
✓ Prefer
fun String.normalizedEmail(): String = trim().lowercase()
class UserRepository(private val database: UserDatabase) {
fun saveEmail(email: String) = database.save(email.normalizedEmail())
}
An extension should naturally describe the receiver. Database or network work belongs to an object whose dependency and responsibility are visible.
A Kotlin function is declared with fun, can have default and named arguments, and may use a block body or expression body. Unit means normal completion without a useful result; Nothing means the function never returns normally. Extension functions improve call-site readability but do not actually add members to a class.
Quick questions
Does an extension function override a member function?
No. A real member wins when both have a matching signature. Extensions are resolved statically using the declared receiver type.
Should public functions rely on inferred return types?
Usually write the return type explicitly. It documents the API and prevents an implementation change from silently changing the exposed type.