Kotlin Koans BR: Default arguments
By Rodrigo Sicarelli 4 min read
🔗 Task
Imagine you have several overloads of foo() in your favorite language.
You can replace all of those overloads with a single function in Kotlin.
Change the declaration of the foo function so that the code using foo compiles.
Java
class OverloadJava {
public String foo(String name, int number, boolean toUpperCase) {
return (toUpperCase ? name.toUpperCase() : name) + number;
}
public String foo(String name, int number) {
return foo(name, number, false);
}
public String foo(String name, boolean toUpperCase) {
return foo(name, 42, toUpperCase);
}
public String foo(String name) {
return foo(name, 42);
}
}
C#
using System;
class OverloadCSharp
{
public string Foo(string name, int number, bool toUpperCase)
{
return (toUpperCase ? name.ToUpper() : name) + number;
}
public string Foo(string name, int number)
{
return Foo(name, number, false);
}
public string Foo(string name, bool toUpperCase)
{
return Foo(name, 42, toUpperCase);
}
public string Foo(string name)
{
return Foo(name, 42);
}
}
Dart
class OverloadDart {
String foo(String name, int number, bool toUpperCase) {
return (toUpperCase ? name.toUpperCase() : name) + number.toString();
}
String foo(String name, int number) {
return foo(name, number, false);
}
String foo(String name, bool toUpperCase) {
return foo(name, 42, toUpperCase);
}
String foo(String name) {
return foo(name, 42);
}
}
Go
package main
import (
"fmt"
"strings"
)
type OverloadGo struct{}
func (s OverloadGo) Foo(name string, number int, toUpperCase bool) string {
if toUpperCase {
return strings.ToUpper(name) + fmt.Sprintf("%d", number)
}
return name + fmt.Sprintf("%d", number)
}
func (s OverloadGo) FooWithNumber(name string, number int) string {
return s.Foo(name, number, false)
}
func (s OverloadGo) FooWithUpperCase(name string, toUpperCase bool) string {
return s.Foo(name, 42, toUpperCase)
}
func (s OverloadGo) FooWithName(name string) string {
return s.Foo(name, 42, false)
}
JavaScript
class OverloadJavaScript {
foo(name, number, toUpperCase) {
return (toUpperCase ? name.toUpperCase() : name) + number;
}
fooWithNameAndNumber(name, number) {
return this.foo(name, number, false);
}
fooWithNameAndUpperCase(name, toUpperCase) {
return this.foo(name, 42, toUpperCase);
}
fooWithName(name) {
return this.foo(name, 42);
}
}
PHP
<?php
class OverloadPHP {
public function foo($name, $number, $toUpperCase) {
return ($toUpperCase ? strtoupper($name) : $name) . $number;
}
public function fooWithNumber($name, $number) {
return $this->foo($name, $number, false);
}
public function fooWithUpperCase($name, $toUpperCase) {
return $this->foo($name, 42, $toUpperCase);
}
public function fooWithName($name) {
return $this->foo($name, 42, false);
}
}
Python
class OverloadPython:
def foo(self, name, number, to_upper_case):
return (name.upper() if to_upper_case else name) + str(number)
def foo_with_number(self, name, number):
return self.foo(name, number, False)
def foo_with_upper_case(self, name, to_upper_case):
return self.foo(name, 42, to_upper_case)
def foo_with_name(self, name):
return self.foo(name, 42, False)
Swift
class OverloadSwift {
func foo(name: String, number: Int, toUpperCase: Bool) -> String {
return (toUpperCase ? name.uppercased() : name) + String(number)
}
func foo(name: String, number: Int) -> String {
return foo(name: name, number: number, toUpperCase: false)
}
func foo(name: String, toUpperCase: Bool) -> String {
return foo(name: name, number: 42, toUpperCase: toUpperCase)
}
func foo(name: String) -> String {
return foo(name: name, number: 42)
}
}
TypeScript
class OverloadTypeScript {
foo(name: string, number: number, toUpperCase: boolean): string {
return (toUpperCase ? name.toUpperCase() : name) + number.toString();
}
fooWithNumber(name: string, number: number): string {
return this.foo(name, number, false);
}
fooWithUpperCase(name: string, toUpperCase: boolean): string {
return this.foo(name, 42, toUpperCase);
}
fooWithName(name: string): string {
return this.foo(name, 42);
}
}
Use case
When we talk about default arguments, we’re referring to a very handy feature in Kotlin.
It lets you skip some arguments when someone calls a function.
If that happens, the compiler uses those default arguments in place of the ones that were skipped.
fun calculateDiscount(price: Double, discountRate: Double = 0.05) = price - price * discountRate
calculateDiscount(price = 50.0)
calculateDiscount(price = 100.0, discountRate = 0.10)
In the example above, the discountRate parameter has a default value of a 5% discount. When you call the calculateDiscount function without specifying the discountRate, the 5% discount is applied to the price.
But when you pass 0.10 as the argument for the discountRate parameter, that’s the value used instead, changing the default discount from 5% to 10%.
Parameter vs Argument
The difference between a parameter and an argument in Kotlin can be understood like this:
- Parameter: identified inside the definition of a function.
- Argument: identified when you invoke or use that function — that is, outside the definition.
Imagine a function that simulates making a coffee:
fun makeCoffee(type: String) = "Making a $type coffee..."
In this definition, type is considered a parameter of the function.
When you request a coffee:
val order = makeCoffee("espresso")
In this context, “espresso” is an argument passed to the makeCoffee() function.
Advantages
- Fewer overloads: lets you have a single function instead of several versions with different arguments.
- Flexibility: you can call the function with different combinations of parameters, as long as the required arguments are provided.
- Java compatibility: functions with default arguments are compatible with Java code, acting as overloads.
Disadvantages
- Code complexity: if overused, they can make the code harder to read and understand.
- Dropped in Java bytecode: In Java, Kotlin’s default arguments aren’t recognized. To work around this, you need to use the
@JvmOverloadsannotation.