28 Oct 2018
7 min read
Kotlin is a language that was built on top of the JVM and what really sets it apart from Java is the emphasis on functions being a first-class construct, meaning many fancy things like lambdas are directly available to Kotlin developers without having to include bulky constructs like the concept of functional interfaces.
In my article on Diving Into Streams, I explained how lambdas were this miraculous constructs in Java that removed a lot of the boilerplate required when passing behavior from one method to another.
However, this definition was really under the assumption of pure Java. Lambdas, as a whole, are a mathematical concept. In programming, a lambda can be seen as a “portable” function or a piece of behavior that can be passed around and used accordingly.
Functions are simply pieces of behavior that take in some input (or none at all) and return some output (or none at all) , this is very much the same as the mathematical concept it is based off of.
Say you were working in a performance-critical situation and you just discovered that there is a major memory issue with a function and it is causing some major performance drawbacks and you want to see how long it takes to benchmark and fix this issue.
“No biggie, I can take the time before and after the execution and check how long that took to benchmark the function” would be something you are thinking to yourself and that would be true in a simple case like ours.
You begin to write some code to test this function:
fun foo(upper: Int): Int {
var sum = 0
for(i in 1..upper) sum += i
return sum
}
fun main(main: Array<String>) {
val before = System.currentTimeMillis()
println("foo returns: ${foo(10000)}")
val after = System.currentTimeMillis()
println("foo took ${after - before}ms to run")
}
Awesome! It worked, you can now go on your merry way and fix the bug. Then you manager comes around and tells you yet another performance issue was found and you think “Eh, this is kind of tedious to redo so I guess I could copy-paste the code I made earlier”.
Now, would you be able to continually repeat this if you suddenly had a whole class of functions that needed to be tested?
Going back to how I mentioned how a lambda is a "portable" function or a piece of behavior that can be passed around and used accordingly.
In this scenario, we ideally, would want to have a function that can time any function with a method signature like
foo()
and this timing function would not need to know anything about the function it receives, all it needs to know
it that it should calculate the time before and after this function took to run and print that out, nicely formatted.
Some basic pseudo-code would look like this:
Details | |
---|---|
Input |
|
Output | None, this function will make use of a side effect |
Steps |
|
In Kotlin, in order to declare a lambda, you have to declare a function type, or some kind of contract for the functions to be passed to follow. To simplify things, all the memory leaks that have surfaced all take in exactly 1 Int input and return an Int, so the function type for this case would look something like:
func: (Int) -> Int
Note how the function type is literally just the method signature of a potential function without the parameter names. As mentioned previously, these function types are merely just contracts, it defines what a function to be passed in should look like, it does not dictate what the inputs will be used for (generally what the parameter names represent) nor does it dictate the specific behaviors of this function.
Now that we have the general structure of the function we want to be able to time, we can then proceed with declaring the function body of this timer.
fun functionTimer(name: String, upper: Int, func: (Int) -> Int) {
val before = System.currentTimeMillis();
println("$name returned ${func(upper)}")
val after = System.currentTimeMillis();
println("$name took ${after - before}ms to run")
}
As you can see, this function’s body is very much the same as the original method of timing the function in the main
method. However, whilst there were some similarities, the benefits of creating a specialized function for timing is as
follows:
upper
argument and apply a consistent argument to be passed to func
so that
the timing is not skewed by changes in the upper, thus ensuring a degree of consistencyWith the newly made timing function, we can then begin to time how long each function took in our class. Let’s begin with the first function we tested, which simply added a bunch of numbers from 1 up till an upper value.
fun main(args: Array<String>) {
functionTimer("add", 10000) {
var sum = 0
for (i in 1..it) sum += i
sum
}
}
What we have used is the lambda syntax Kotlin provides. We declare the arguments of the functionTimer
function as per
usual, but we then use {}
which houses the body of the function. We also use the it
keyword, to refer to the one and
only input this lambda receives, which we can assume would be the upper limit (in this case, 10000).
Another thing to note is we omit the return
keyword from the lambda body even though the function type specifies that
each function has to return an Int
. We simply put the value to be returned on a line of its own and Kotlin is clever
enough to know to return this value.
Now, we can extend this function to work with any other function that takes in a single Int input and return an Int…
functionTimer("multiply", 100) {
var result = 1
for (i in 2..it) result *= i
result
}
functionTimer("wacky", 1000) { (1..it).map { num -> num * 3 }.sum() }
Sample output:
add returned 50005000
add took 1ms to run
multiply returned 0
multiply took 0ms to run
wacky returned 1501500
wacky took 58ms to run
You can see how flexible we can make this timer be, taking in all sorts of functions and timing them.
Sometimes, you might not want to rewrite a long function just to time it, and that is perfectly fine, you can pass a reference to an existing method for it to be run as such:
functionTimer("add", 10000, ::add)
Lambdas are far more than just useful for timing stuff, it is extremely versatile and Kotlin uses them extensively throughout their language, in Collections to APIs that leverage off the design of lambdas.
Some resources for further reading:
Enjoyed reading?
Consider subscribing to my RSS feed or reaching out to me through email!