Why functional programming?
Deep Concept of Functional programming
Functional programming has been around for almost as long as other programming paradigms, such as procedural and object-oriented programming, if not longer. But in the past 10 years, it has gained major momentum. The reason for that is because something else stalled: CPU speeds. We cannot speed up our CPUs as much as we did in the past, so we must parallelize our programs. And it turns out that the functional programming paradigm is exceptional at running parallel tasks. The evolution of multicore processors is a very interesting topic by itself, but we'll be able to cover it only briefly. Workstations had multiple processors since the 1980s at least, to support running tasks from different users in parallel. Since workstations were huge anyway, they didn't need to worry about cramming everything into one chip. But with multiprocessors coming to the consumer market around 2005, it was necessary to have one physical unit that could do work in parallel. That's the reason we have multiple cores on one chip in our PC or laptop.
But that's not the only reason some swear by functional programming. Here are a few more:
Functional programming favors pure functions, and pure functions are usually easier to reason about and to test
Code written in a functional way is often more declarative than imperative, dealing with the what and not the how
We have extensively studied functional programming, and anyone working in Android app development and as a Kotlin developer for back-end frameworks like Ktor knows the significant advantages that functional programming offers compared to procedural programming languages.
Immutability
One of the key concepts of functional programming is immutability. It means that from the moment the function receives input to the moment the function returns output, the object doesn't change. How could it change, you wonder? Let's see a simple example: fun <T> printAndClear(list: MutableList<T>) {
for (e in list) {
println(e)
list.remove(e)
}
}
printAndClear(mutableListOf("a", "b", "c")) The output would be first "a", then we'll receive ConcurrentModificationException. Wouldn't it be great if we could protect ourselves from such runtime exceptions in the first place?
Tuples
In functional programming, a tuple is a piece of data that cannot be changed after it is created. One of the most basic tuples in Kotlin is Pair: val pair = "a" to 1 Pair contains two properties, first and second, and is immutable: pair.first = "b" // Doesn't work
pair.second = 2 // Still doesn't We can destructure a Pair into two separate values: val (key, value) = pair
println("$key => $value") When iterating over a map, we receive another tuple, Map.Entry: for (p in mapOf(1 to "Sunday", 2 to "Monday")) {
println("${p.key} ${p.value}")
}
In general, data classes are usually a good implementation for tuples. But, as we'll see in the Value Mutation section, not every data class is a proper tuple.
Value mutation
In our earlier blog game we design as Maronic, we would like to count the average score over one thousand games. For that, we have the following
data class: data class AverageScore(var totalScore: Int = 0,
var gamesPlayed: Int = 0) {
val average: Int
get() = if (gamesPlayed <= 0)
0
else
totalScore / gamesPlayed
}
We were smart: we protected ourselves from any invalid output by checking for divisions by zero. But what will happen when we write the following code?
val counter = AverageScore()
thread(isDaemon = true) {
while(true) counter.gamesPlayed = 0
}
for (i in 1..1_000) {
counter.totalScore += Random().nextInt(100)
counter.gamesPlayed++
println(counter.average)
}
Soon enough, you'll receive ArithmeticException anyway. Our counter somehow becomes zero. If you want your data classes to be immutable, be sure to specify all their properties as val (values), and not var (variables).
Immutable collections
I think that our junior developer learned their lesson. Instead, they produced this code, which is not very efficient, but which gets rid of those variables:
data class ScoreCollector(val scores: MutableList<Int> = mutableListOf())
val counter = ScoreCollector()
for (i in 1..1_000) {
counter.scores += Random().nextInt(100)
println(counter.scores.sumBy { it } / counter.scores.size)
}
But the maleficent thread strikes again:
thread(isDaemon= true, name="Maleficent") {
while(true) counter.scores.clear()
}
We again receive ArithmeticException.It's not enough that your data class contains only values. If its value is a collection, it must be immutable in order for the data class to be considered immutable. The same rule is applied to classes contained within other data classes:
data class ImmutableScoreCollector(val scores: List<Int>)
Now the maleficent thread cannot even call clear() on this collection. But how should we add scores to it? One option is to pass the entire list in the constructor:
val counter = ImmutableScoreCollector(List(1_000) {
Random().nextInt(100)
})
I am not going to tell you about Kotlin as it is very clearly written on Kotlin Developer site .However i have tried to give gist on Functional programming .
Functions as values
We already covered some of the functional capabilities of Kotlin to our earlier blog in Design Patterns. The Strategy and Command design patterns are but a few that heavily rely on the ability to accept functions as arguments, return functions, store them as values, or put them inside collections. In this section, we'll cover some other aspects of functional programming in Kotlin, such as function purity and currying.
Higher-order functions
As we discussed previously, in Kotlin, it's possible for a function to return another function:
fun generateMultiply(): (Int, Int) -> Int {
return { x: Int, y: Int -> x * y}
}
Functions can also be assigned to a variable or value to be invoked later on:
val multiplyFunction = generateMultiply()
...
println(multiplyFunction(3, 4))
The function assigned to a variable is usually called a literal function. It's also possible to specify a function as a parameter:
fun mathInvoker(x: Int, y: Int, mathFunction: (Int, Int) -> Int) {
println(mathFunction(x, y))
}
mathInvoker(5, 6, multiplyFunction) If a function is the last parameter, it can also be supplied ad hoc, outside of the brackets: mathInvoker(7, 8) { x, y ->
x * y
}
In general, a function without a name is called an anonymous function. If a function without a name uses short syntax, it's called a lambda: val squareAnonymous = fun(x: Int) = x * x
val squareLambda = {x: Int -> x * x}
Pure functions
A pure function is a function without any side effects. Take the following function, for example:
fun sayHello() {
println("Hello")
}
How do you test to see whether "Hello" is indeed printed? The task is not as simple as it seems, as we'll need some means to capture the standard output, the same console where we usually see stuff printed. Compare it to the following function: fun hello() = "Hello" The following function doesn't have any side effects. That makes it a lot easier to test:
fun testHello(): Boolean {
return "Hello" == hello()
}
Does the hello() function look a bit meaningless to your eyes? That's actually one of the properties of pure functions.
Their invocation could be replaced by their result (if we knew all their results, that is). This is often called referential transparency. Not every function written in Kotlin is pure:
fun <T> removeFirst(list: MutableList<T>): T {
return list.removeAt(0)
}
If we call the function twice on the same list, it will return different results:
val list = mutableListOf(1, 2, 3)
println(removeFirst(list)) // Prints 1
println(removeFirst(list)) // Prints 2 Try this one: fun <T> withoutFirst(list: List<T>): T {
return ArrayList(list).removeAt(0)
}
Now our function is totally predictable, no matter how many times we invoke it: val list = mutableListOf(1, 2, 3)
println(withoutFirst(list)) // It's 1
println(withoutFirst(list)) // Still 1
As you can see, we used an immutable interface this time, List<T>, which helps us by preventing even the possibility of mutating our input. Together with immutable values from the previous section, pure functions provide a very strong tool that allows easier testing by providing predictable results and parallelization of our algorithms.
Currying
Currying is a way to translate a function that takes a number of arguments into a chain of functions that each take a single argument. This may sound confusing, so let's look at a simple example:
fun subtract(x: Int, y: Int): Int {
return x - y
}
println(subtract(50, 8))
This is a function that returns two arguments. The result is quite obvious. But maybe we would like to invoke this function with the following syntax instead: subtract(50)(8) We've already seen how we can return a function from another function:
fun subtract(x: Int): (Int) -> Int {
return fun(y: Int): Int {
return x - y
}
}
Here it is in the shorter form: fun subtract(x: Int) = fun(y: Int): Int {
return x + y
}
And here it is in an even shorter form: fun subtract(x: Int) = {y: Int -> x - y} Although not very useful by itself, it's still an interesting concept to grasp. And if you're a JavaScript developer looking for a new job, make sure you understand it really well, since it's being asked about in nearly every interview.
Memorization
If our function always returns the same output for the same input, we could easily map between previous input and output, and use it as a cache. That technique is called memoization:
class Summarizer {
private val resultsCache = mutableMapOf<List<Int>, Double>()
fun summarize(numbers: List<Int>): Double {
return resultsCache.computeIfAbsent(numbers, ::sum)
}
private fun sum(numbers: List<Int>): Double {
return numbers.sumByDouble { it.toDouble() }
}
}
We use a method reference operator, ::, to tell computeIfAbsent to use the sum() method in the event that input wasn't cached yet. Note that sum() is a pure function, while summarize() is not. The latter will behave differently for the same input. But that's exactly what we want in this case: val l1 = listOf(1, 2, 3)
val l2 = listOf(1, 2, 3)
val l3 = listOf(1, 2, 3, 4)
val summarizer = Summarizer()
println(summarizer.summarize(l1)) // Computes, new input
println(summarizer.summarize(l1)) // Object is the same, no compute
println(summarizer.summarize(l2)) // Value is the same, no compute
println(summarizer.summarize(l3))
Computes The combination of immutable objects, pure functions, and plain old classes provides us with a powerful tool for performance optimizations. Just remember, nothing is free. We only trade one resource, CPU time, for another resource, memory. And it's up to you to decide which resource is more expensive for you in each case.
Expressions,
expressions not statements A statement is a block of code that doesn't return anything. An expression, on the other hand, returns a new value. Since statements produce no results, the only way for them to be useful is to mutate state. And functional programming tries to avoid mutating the state as much as possible. Theoretically, the more we rely on expressions, the more our functions will be pure, with all the benefits of functional purity. We've used the if expression many times already, so one of its benefits should be clear: it's less verbose and, for that reason, less error-prone than the if statement. Pattern matching The concept of pattern matching is like switch/case on steroids for someone who comes from Java. We've already seen how when expression can be used, in Chapter 1, Getting Started with Kotlin, so let's briefly discuss why this concept is important for the functional paradigm. As you may know, switch in Java accepts only some primitive types, strings, or enums. Consider the following code in Java:
class Cat implements Animal {
public String purr() {
return "Purr-purr";
}
}
class Dog implements Animal {
public String bark() {
return "Bark-bark";
} }
interface Animal {}
If we were to decide which of the functions to call, we would need something like this:
public String getSound(Animal animal) {
String sound = null;
if (animal instanceof Cat) {
sound = ((Cat)animal).purr();
}
else if (animal instanceof Dog) {
sound = ((Dog)animal).bark();
}
if (sound == null) {
throw new RuntimeException();
}
return sound;
}
This method could be shortened by introducing multiple returns, but in real projects, multiple returns are usually bad practice. Since we don't have a switch statement for classes, we need to use an if statement instead. Compare that with the following Kotlin code:
fun getSound(animal: Animal) = when(animal) {
is Cat -> animal.purr()
is Dog -> animal.bark()
else -> throw RuntimeException()
}
Since when is an expression, we avoided the intermediate variable altogether. But what's more, using pattern matching, we can also avoid most of the code that concerns type checks and casts.
Recursion
Recursion is a function invoking itself with new arguments:
fun sumRec(i: Int, numbers: List<Int>): Long {
return if (i == numbers.size) {
0
} else {
numbers[i] + sumRec(i + 1, numbers)
}
}
We usually avoid recursion, due to Stack Overflow error that we may receive if our call stack is too deep. You can call this function with a list that contains a million numbers to experience it:
val numbers = List(1_000_000) {it}
println(sumRec(0, numbers)) // Crashed pretty soon, around 7K
One of the great benefits of tail recursion is that it avoids the dreaded stack overflow exception. Let's rewrite our recursive function using a new keyword, tailrec, to avoid that problem:
tailrec fun sumRec(i: Int, sum: Long, numbers: List<Int>): Long {
return if (i == numbers.size) {
return sum
} else {
sumRec(i+1, numbers[i] + sum, numbers)
}
}
Now the compiler will optimize our call and avoid exception completely. Summary You should now have a better understanding of functional programming and its benefits. We've discussed the concepts of immutability and pure functions. A combination of the two often results in more testable code, which is easier to maintain. Currying and memoization are two useful patterns that originate from functional programming. Kotlin has a tailrec keyword that allows the compiler to optimize tail recursion. We also looked at higher-order functions, expressions versus statements, and pattern matching. In the next chapter, we'll put this knowledge to practical use, and discover how reactive programming builds upon functional programming in order to create scalable and resilient systems.
No comments:
Post a Comment