In our previous example of Chess, we defined concrete strategies by implementing the BotBehavior interface. This interface was quite simple and defined only a single method. In situations like these, we could simplify our Strategy pattern implementation by relying on higher-order functions instead of concrete types. Let's see how this is done:
- Let's create a new chess class called FunctionalChess:
class FunctionalChess(val behavior: (Array<Array<Int>>) -> Unit) {
fun play() {
// game loop
// relies on behavior
}
fun pause() { }
}
This new FunctionalChess class takes a function parameter instead of an instance of BotBehavior. This will enable us to define our behavior strategies as functions and pass them to FunctionalChess.
- We can now define an easy behavior and a difficult behavior as top-level function variables:
val easyBehavior:(Array<Array<Int>>) -> Unit = { gameState ->
// pick first available action
}
val difficultBehavior:(Array<Array<Int>>) -> Unit = { gameState ->
// perform look ahead search
}
- Now, we are able to use those defined function variables to configure how our FunctionalGame class should operate:
fun main() {
val functionalGame = FunctionalChess(easyBehavior)
functionalGame.play()
functionalGame.pause()
functionalGame.behavior = difficultBehavior
}
As in the previous example, we can swap out behaviors as needed to control how the game plays, but we don't need to create several new classes in order to do so. We are able to rely on simple functions.
This pattern of relying on higher-order functions to control the behavior of an algorithm is how the Kotlin standard library works. If we look at the sortedBy() function from the Kotlin standard library, we can see that it takes a function parameter that helps determine how elements should be sorted. Consider the following code snippet:
/**
* Returns a list of all elements sorted according to natural sort order of the value returned by specified [selector] function.
*
* The sort is _stable_. It means that equal elements preserve their order relative to each other after sorting.
*/
public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) -> R?): List<T> {
return sortedWith(compareBy(selector))
}
From the preceding code, we can see that the function allows us to change how elements are sorted by changing the lambda passed to our call to sortedBy(). If we look at the following example, we'll see we are choosing to sort based on the length of the string:
listOf("programming languages", "foo", "Kotlin")
.sortedBy { it.length }
.forEach { println(it) }
// prints "foo", "Kotlin", "programming languages"
However, if we sort based on the value of the first character in the string, we get a different output:
listOf("programming languages", "foo", "Kotlin")
.sortedBy { it[0] }
.forEach { println(it) }
// prints "Kotlin", "foo", "programming languages"
By changing the strategy defined by our function parameter, we were able to change the output of the operation. This flexibility is really powerful. By writing code that defers key decisions to swappable components such as an interface or higher-order function, we can make our code more flexible and easier to reuse.