Function types

In programming terms, a function is a block of code that performs a task. Functions are reusable and can be run again and again throughout the lifetime of the application. A function's behaviour can be customised using arguments, which describe data that must be supplied when the function is called, and output, which is data that is returned by the function once the task is complete. If a function belongs to a class or object, then it is referred to as a method.

function-types.png

Top-level functions

Top-level functions are standalone functions that exist independently of classes, objects and interfaces. Top-level functions are defined directly in a code file, and often server helper or utility roles. For example, the below code shows an example top-level function that you could add to a Kotlin (*.kt) file to print the current system timestamp to the console.

import java.time.LocalDateTime

// Defined within a *.kt file
fun printTimestamp() {
	println(LocalDateTime.now())
}

Extension functions

Extension functions allow you to define additional behaviours for existing data types. In doing so, extension functions allow you to easily add new capabilities to existing classes, even if you did not design the class. For example, the below Kotlin code shows how you could define an extension function for the CharSequence class called forEach, which iterates through each character in a sequence of characters.

fun CharSequence.forEach(action: (Char) -> Unit): Unit {
    for (element in this) action(element)
}

fun main(args: Array<String>) {
	val charSequence: CharSequence = "Loading..."
	
	// The below will print each character to the console on a new line
	charSequence.forEach{ char ->
		println(char)		
	}
}

Higher-order functions

Higher-order functions are a feature of programming languages such as Kotlin and JavaScript. Higher-order functions allow you to use functions as arguments and return types. For example, the below JavaScript code shows how you could pass a function called printTickets as an argument to a function called purchaseTickets. The purchaseTickets function can then use the printTickets function via the argument.

function printTickets() {
	console.log("Printing...");
	console.log("Ticket printed!");
}

function purchaseTickets(quantity, action) {
    for (let i = 0; i < quantity; i++) {
	    action();
	}
}

/* prints:
Printing...
Ticket printed!
Printing...
Ticket printed!
Printing...
Ticket printed!
*/
purchaseTickets(3, printTickets);

The below Kotlin code demonstrates how to use higher-order functions to use one function to return another function:

fun joinFirstLastNames(firstName: String, lastName: String): String {
    return "$firstName $lastName"
}

fun getFunction(): ((String, String)-> String) {
    return ::joinFirstLastNames
}

fun main(args: Array<String>) {
    val joinNamesFunction = getFunction()
	// Prints 'John Smith'
	println(joinNamesFunction("John", "Smith"))
}

First-class functions

First-class functions are a feature of programming paradigms such as functional programming. In brief, first-class functions are treated like other conventional data types. First-class functions can be stored as variables, passed as arguments to other functions, returned by other functions, and stored within other data structures. For example, the programming language Kotlin treats functions as first-class citizens, and the principles of first-class functions are demonstrated in the below code:

// First-class functions can be used as arguments for other functions
fun CharSequence.forEach(action: (Char) -> Unit): Unit {
    for (element in this) action(element)
}
				
fun joinFirstLastNames(firstName: String, lastName: String): String {
    return "$firstName $lastName"
}

// First-class functions can be returned from other functions
fun getFunction(): ((String, String)-> String) {
    return ::joinFirstLastNames
}

fun main(args: Array<String>) {
	// First-class functions can be assigned to variables
    val joinNamesFunction = getFunction()
	
	// First-class functions can be stored in other data structures
	val listOfFunctions = listOf(joinNamesFunction, getFunction(), getFunction())
	
    /*
     *  Prints:
     * 	J
     * 	o
     * 	h
     * 	n
     * 
     * 	S
     * 	m
     * 	i
     * 	t
     * 	h 
     */
	listOfFunctions[0]("John", "Smith").forEach{ char ->
		println(char)		
	}
}

Local functions

Local functions are functions that exist within another function. The visibility of a local function cannot be modified, and it is only accessible to the parent function. Local functions are useful for repetitive actions that the parent function may need to perform on multiple occasions. For example, the below code defines a local function called addTenYearsToAge, which is accessible only to the parent function printFutureAge.

fun printFutureAge(name: String, age: Int) {
    // Local function
	fun addTenYearsToAge(initialAge: Int): Int {
		return initialAge + 10
	}
	
	println("In ten years, $name will be " + addTenYearsToAge(age)
		+ " years old.")
}

fun main(args: Array<String>) {
	// Prints: In ten years, Jane will be 34 years old
    printFutureAge("Jane", 24)
}

Single expression functions

Single expression functions are a feature of programming languages such as Kotlin and serve as a means of reducing boilerplate (repetitive) code. Single expression functions are suitable for function bodies that comprise a single line of code. You simply remove the curly braces and insert an equal sign before the function body. For example, the below Kotlin code demonstrates a traditional Kotlin function:

fun joinFirstLastNames(firstName: String, lastName: String): String {
    return "$firstName $lastName"
}

The function body of the joinFirstLastNames function comprises a single line, and so could be converted to a single expression function using the following Kotlin code:

fun joinFirstLastNames(firstName: String, lastName: String): String = "$firstName $lastName"

Infix functions

Infix functions provide a means of writing simpler, more readable code by allowing the period and brackets to be omitted when the function is called. To write an infix function, you must ensure the function is associated with a class and accepts a single parameter. The parameter must not be a varargs parameter and the parameter cannot be assigned a default value. For example, the below Kotlin code defines a class called Person that features two methods (a method is a function that belongs to a class). Both methods perform the same action: they accept a new value for the person's surname and then print the person's new full name to the console. The first method is defined traditionally, while the second method uses the infix notation. In using the infix notation, we can call the function by simply typing the class instance, name of the function, and parameter value, as shown below:

class Person (val firstName: String, val lastName: String) {
	fun traditionalChangeLastName(newLastName: String) {
		println("Name successfully changed to $firstName $newLastName")
	}
	
	infix fun infixChangeLastName(newLastName: String) {
		println("Name successfully changed to $firstName $newLastName")
	}
}
				
fun main(args: Array<String>) {
	val person = Person("Robin", "Waters")
	
	// Traditional function approach
	// Prints: Name successfully changed to Robin Jones
    person.traditionalChangeLastName("Jones")
	
	// Infix function approach
	// Prints: Name successfully changed to Robin Jones
    person infixChangeLastName "Jones"
}