Now that we've reviewed working with functional types, let's take a look at how to use those functional types as arguments to other functions.
Here's a basic example of a higher-order function in which we've defined a function parameter matching the function signature of our previously defined onClickHandler variable:
class ViewModel(val viewState: ViewState, val clickHandler:(ViewState) -> Unit)
fun createViewModel(viewState: ViewState, clickHandler: (ViewState) -> Unit) : ViewModel {
return ViewModel(viewState, clickHandler)
}
We could then pass our onClickHandler function variable as an argument to this newly defined createViewModel() function:
fun main() {
...
createViewModel(viewState, onClickHandler)
}
As with any other type, we can store functions as variables and then pass them anywhere that expects that defined type. Function parameters allow us to do some interesting things. Once such example is how we can construct and configure a class using a functional constructor parameter:
class LoadingViewModel(config: (LoadingViewModel) -> Unit) {
var title = ""
var subtitle = ""
var loadingMsg = ""
val successMsg = ""
init {
config(this)
}
}
This LoadingViewModel has several properties available that may, or may not, be needed when it's used. Rather than forcing clients to specify the value of each property when the class is instantiated, we could create and configure an instance of LoadingViewModel like this:
LoadingViewModel { loadingViewModel ->
loadingViewModel.title = "Hello"
loadingViewModel.loadingMsg "Loading..."
}
This makes use of lambda syntax and the function parameter to allow us to instantiate LoadingViewModel without the () and then configure the properties within the passed lambda. This syntax begins to feel very fluent and natural within a more functional code base as it's really just a function that is configuring the class.
As we saw in the previous chapter, we can improve on this configuration pattern by making use of a functional type with a receiver. We can refactor our function parameter, as follows, so that instead of taking a LoadingViewModel as its only parameter, it will have an instance of LoadingViewModel as its receiver:
class LoadingViewModel(config: LoadingViewModel.() -> Unit) {
var title = ""
var subtitle = ""
var loadingMsg = ""
val successMsg = ""
init {
config(this)
}
}
Our config parameter now takes a function that requires an instance of LoadingViewModel as a receiver, and we can then pass the current instance to the config function within our class's init{} block.
By making use of a function type with a receiver, we remove the need to explicitly reference the passed LoadingViewModel using this:
LoadingViewModel {
this.title = "Hello"
this.loadingMsg = "Loading..."
}
We can reference the LoadingViewModel instance implicitly within the lambda context. With this in mind, our previous example can be simplified even further, as follows:
LoadingViewModel {
title = "Hello"
loadingMsg = "Loading..."
}
Notice that in this latest example, we've removed the explicit usage of this when referencing the title and loadingMsg properties. With this, and other, examples of higher-order functions, we've seen how they can be incredibly useful when we design and consume our classes and APIs. In the next section, we'll look at how to ensure that our higher-order functions don't negatively impact the performance of our programs.