Unit
. The Unit
type has only one value, also called Unit
.Unit
. In this way, one can say that every Kotlin function returns a value.We have called Kotlin functions already. For example,
print("Value of a is ") // line 1
print(1) // line 2
println() // line 3
print
function called in Line 1 takes a String argument and returns Unit.print
function called in Line 2 takes an Int argument and returns Unit.println
function called in Line 3 takes no argument and returns Unit.The print
function in Line 1 is not the same as the print
function in Line 2. Kotlin allows two different functions to have the same name so long as they have different lists of argument types. Such functions are said to be overloaded.
Here is how we write function types in Kotlin:
() -> Unit
() -> Int
(Double) -> String
(Int, Double) -> Unit
()
and arrow ->
symbols are necessary. They cannot be omitted.() -> Unit
is the function type that takes no argument and returns no useful value.() -> Int
is the function type that takes no argument and returns an Int value.(Double) -> String
is the function type that takes a Double argument and returns a String value.(Int, Double) -> Unit
is the function type that takes two arguments, the first of which is an Int and the second of which is a Double, and returns no useful value.
name : type
,->
, andThis lambda expression denotes a function that takes no argument and returns Unit
:
{ -> println("My type is () -> Unit") }
It can be abbreviated as
{ println("My type is () -> Unit") }
This lambda expression denotes a function that takes no argument and returns an Int:
{ -> 42 }
It can be abbreviated as
{ 42 }
Note that for functions that take no arguments, the arrow can be omitted.
E.g., this lambda expression
{ print("hi"); 1 }
has type
() -> kotlin.Int
It represents the function that takes no argument, prints the String “hi” to the screen, and returns 1.
->
.->
as well. In such a case, we use the default name it
for that parameter.Here is an example. We want to define a function twice
that takes a single Int
argument and returns an Int
result whose value is twice that of the argument. We can write twice
like this
val twice: (Int) -> Int = { 2 * it }
instead of the longer
val twice: (Int) -> Int = { i: Int -> 2 * i }
Let’s look at this example anonymous function:
fun (): Int {
print("hi")
return 1
}
{ print("hi"); 1 }
.fun
followed by a list of comma-separated arguments with type annotations enclosed in a pair of parentheses, followed by a colon and the return type. It finally ends in a block body.Within its body there must be at least one return statement if the function returns a non-Unit value.
This is an example anonymous function
fun (name: String, score: Int): String {
println("$name scored $score")
return if (score > 95) "exceptional"
else if (score > 90) "excellent"
else "ok"
}
In case the function returns Unit, we can omit the return type annotation : Unit
and the return statement return Unit
. For example, instead of writing
fun (): Unit {
print("Hi there")
return Unit
}
we can write
fun () {
print("Hi there")
}
instead.
var
or val
, then write the function name (and type annotation if needed), then write =
, then write either a lambda expression or an anonymous function.E.g. either one of these two ways work fine:
val f: () -> Unit = { print("Hi there") }
val f = fun (): Unit { print("Hi there") }
With the help of type inference, we can write even more succinctly like this:
val f = { print("Hi there") }
or this:
val f = fun () { print("Hi there") }
Another way to define a function is to take an anonymous function and insert the name of the function to be defined after the keyword fun
, e.g.,
fun f(): Unit { print("Hi there") }
Of course, since f
returns Unit
, its return type can be omitted to get
fun f() { print("Hi there") }
This is the usual way people define functions in Kotlin!
One syntactic sugar is that if the body of the function consists of only one expression, we can remove the braces and use the = sign to define the function, like this:
fun double(n: Int): Int = 2 * n
One further simplification is that in case the return type can be inferred from the expression, we can omit it all together. So the above can be written
fun double(n: Int) = 2 * n
Functions are called by writing any expression that evaluates to a function value, followed by the comma-delimited arguments, surrounded by a pair of parentheses. E.g.,
val f: (Int) -> Int = { 2 * it }
// The following call to f returns an Int of value 6.
f(3)
fun g(i: Int, s: String, d: Double) { println("$s $i items is $d") }
/*
The following function call to g will print the string
"The average of 5 items is 1.414", without quotes,
and return nothing useful.
*/
g(3 + 2, "The average of", 1.414)
This method of calling functions uses positional arguments. Arguments are given in the same number, type and order as when the function was defined.
The parens are needed even for functions that take no arguments, e.g.,
println()
One calls member functions
by using the dot notation, e.g.,
val s: String = "hello"
if (s.startsWith("a"))
println("$s starts with 'a'")
Function parameters can be specified to take on default values. These default values are used when the corresponding arguments are omitted at function call. E.g., if a function f
is defined as
fun f(s: String, i: Int = 2, d: Double = 3.14) { ... }
Then f
can be called by giving it 1, 2 or 3 arguments. E.g., all these 3 function calls
f("hello")
f("hello", 2)
f("hello", 2, 3.14)
are equivalent.
Functions can also be called by providing the arguments in the “argument = value” syntax. E.g., for the function f
defined here
fun f(s: String, i: Int = 2, d: Double = 3.14) { ... }
we can call it in many different ways, e.g.,
f(i = 3, s = "Alice", d = 1.2) // line 1
f(d = 2.1, s = "Bob") // line 2
f("Charlie", d = 0.3) // line 3
The call in line 3 shows that we can combine positional arguments and named arguments in the same call, as long as we provide all the positional arguments first.
g
can be defined locally within the definition of another function f
.g
is a local function of f
, i.e., it is visible only within f
starting from g
’s point of definition.g
can see all definitions of f
that occur before g
’s point of definition.Here is a simple example of nested function.
fun main(args: Array<String>) {
val basic = "ho"
fun three(s: String) = s.repeat(3)
fun ten(s: String) = "$s ".repeat(10)
fun greeting() = ten(three(basic))
println(greeting())
}
This program prints
hohoho hohoho hohoho hohoho hohoho hohoho hohoho hohoho hohoho hohoho
Thus, higher-order functions will have types that look something like
(Int) -> (Int) -> Int
((Int) -> Int) -> Int
(Int, (Int, Double) -> Char) -> String
(Int, Int, Double) -> (Char) -> String
((Double) -> Double, (Double) -> Double) -> (Double) -> Double
(((Double) -> Double, (Double) -> Double) -> Double) -> Double
IntArray
as an example but what we’ll learn will be applicable to constructors of all array types.Syntactically, a constructor is called with the type name, e.g. the constructor for an IntArray
is called IntArray
. The IntArray
constructor has this signature
<init>(size: Int, init: (Int) -> Int = { 0 })
size
and has type Int
; it specifies how many Ints the array will contain.init
and has type (Int) -> Int
, and, unless told otherwise, will initialize all array elements to 0 by default.init
function parameter takes an argument which is the array index, and returns a value to be initialized for that indexed element.Here are some example calls to construct new IntArray’s.
/*
* The init function is not given, so `a1` will be a new array of 3 Ints,
* with each element initialized to 0.
*/
val a1 = IntArray(3)
/*
* The init function is given, so `a2` will be a new array of 3 Ints,
* with elements initialized so that a2[0] = 0, a2[1] = 1, a2[2] = 2.
*/
val a2 = IntArray(3, { it })
/*
* The init function is given, so a3 will be a new array of 3 Ints,
* with elements initialized so that a3[0] = 3, a3[1] = 2, a3[2] = 1.
*/
val a3 = IntArray(3, { 3-it })
When the last argument of a function call is a lambda expression, Kotlin allows us to write it outside of the parens. E.g., the line
val a3 = IntArray(3, { 3-it })
can be written as
val a3 = IntArray(3) {
3-it
}
and this is the preferred way to write such a function call.
IntArray
applies to constructors for arrays of other types as well.E.g., to create an array of five String
s and initialize every element to an empty string, you should write
val stringArray = Array<String>(5) { "" }
arrayOf
that can construct an array of any type, and at the same time initialize their elements to the values you provide with the call.E.g., this line
val names = arrayOf("Alice", "Bob", "Charlie")
will create an array of 3 strings and assign it to the variable names
. It also initializes names[0]
with the string “Alice”, names[1]
with “Bob”, and names[2]
with “Charlie”.
E.g., a two-dimensional array (what we call matrix in mathematics) of Int
s is simply an array of IntArray
, i.e., its type is
Array<IntArray>
Here are some example creation of 2d-arrays in Kotlin.
// The 3x3 zero matrix.
val zero3 = Array<IntArray>(3) { IntArray(3) }
// The 3x3 identity matrix.
val id3 = Array<IntArray>(3) { i ->
IntArray(3) { j ->
if (i == j) 1 else 0
}
}
Here is a special case of function composition in Kotlin:
fun o(
f: (Double) -> Double,
g: (Double) -> Double): (Double)->Double = { g(f(it)) }
Double
.With the definition of o
above, we can now write
fun f(x: Double) = x + 2.0
fun g(x: Double) = x * x
println(o(::f, ::g)(1.0)) // This will print 9.0
The ::
is the function reference operator. Written in front of a function name, it means we want to reference that function instead of calling it.