Kotlin Functional Programming
Note this blog post contains my notes from the Functional Programming section of the book Atomic Kotlin. Consider buying a copy of the book for a more detailed reference.
Lambdas
Lambdas produce compact code that is easier to understand.
Lambdas are AKA function literal.
Lambdas require a minimal amount of code to create and can be inserted directly into other code.
The map() function works with collections such as List. It applies a tranasformation function to each element in the collection returning a new List.
Example:
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4)
val result = list.map({n: Int -> "[$n]" })
result eq listOf("[1]", "[2]", "[3]", "[4]")
}
The parameter list is separated by the function body by an arrow
The function body can be one or more expressions. The final expression becomes the return value of the lambda.
Simpler syntax where type inference is avialable:
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4)
val result = list.map({ n -> ["$n]"})
result eq listOf("[1]", "[2]", "[3]", "[4]")
}
If there’s only a single parameter, Kotlin generates the name it for that parameter resulting in yet simpler syntax
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4)
val result = list.map({ "[$it]" })
result eq listOf("[1]", "[2]", "[3]", "[4]")
}
If the lambda is a trailing arg to a function or the only arg to a function then you can remove the parentheses.
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4)
val result = list.map { "[$it]" }
result eq listOf("[1]", "[2]", "[3]", "[4]")
}
If the function takes more than one arg, all except the last lambda arg must be in parentheses.
Example with joinToString():
import atomictest.eq
fun main() {
val list = listOf(9, 11, 23, 32)
list.joinToString(" ") { "[$it]" } eq
"[9] [11] [23] [32]"
}
If you want to provide the lambda as a named arg you must place the lambda in parentheses
import atomictest.eq
fun main() {
val list = listOf(9, 11, 23, 32)
list.joinToString(
separator = " ",
transform = { "[$it]" }
) eq "[9] [11] [23] [32]
}
Syntax for a lambda with more than one parameter:
import atomictest.eq
fun main() {
val list = listOf('a', 'b', 'c')
list.mapIndexed { index, element ->
"[$index: $element]"
} eq listOf("[0: a]", "[1: b]", "[2: c]")
}
You can ignore particular args with an _
import atomictest.eq
fun main() {
val list = listOf('a', 'b', 'c')
list.mapIndexed { index, _ ->
"[$index]"
} eq listOf("[0]", "[1]", "[2]")
}
Alternative way of writing the above:
import atomictest.eq
fun main() {
val list = listOf('a', 'b', 'c')
list.indices.map {
"[$it]"
} eq listOf("[0]", "[1]", "[2]")
}
Lambdas can have zero args as well. Then the arrow is optional but recommended:
import atomictest.eq
fun main() {
run { -> trace("A lambda") }
run { trace("Without args") }
trace eq """
A lambda
Without args
"""
}
run simply calls its lambda arg
Example of a lambda to select only even elements:
Without Lambda:
package importanceoflambdas
import atomictest.eq
fun filterEven(nums: List<Int>): List<Int> {
val result = mutableList<Int>()
for (i in nums) {
if (i % 2 == 0)
result.add(i)
}
return result
}
fun main() {
filterEven(listOf(1, 2, 3, 4)) eq
listOf(2, 4)
}
With Lambda:
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4)
val even = list.filter { it % 2 == 0 }
even eq listOf(2, 4)
}
Functional programming solves problems in small steps.
You can store a lambda in var or val:
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4)
val transform = { n: Int -> "[$n]" }
val result = list.map(transform)
result eq
listOf("[1]", "[2]", "[3]", "[4]")
}
Lambdas can refer to elements outside their scope. When a function closes over elements in its environment we call it a closure.
Lambda’s and closures are distinct features.
Example of a lambda that closes over environment:
import atomictest.eq
fun main() {
val list = listOf(1, 5, 7, 10)
val divider = 5
list.filter { it % divider == 0 } eq
listOf(5, 10)
}
In the above example the lambda captures divider
Lambdas can modify captured values like sum below:
import atomictest.eq
fun main() {
val list = listOf(1, 5, 7, 10)
val sum = 0
val divider = 5
list.filter { it % divider == 0 }
.forEach { sum += it }
sum eq 15
}
Alternative code without changing the state of sum in the environment:
import atomictest.eq
fun main() {
val list = listOf(1, 5, 7, 10)
val divider = 5
list.filter { it % divider == 0 }
.sum() eq 15
}
An ordinary function can also close over its env
Operations on Collections
We can create lists in many ways:
import atomictest.eq
fun main() {
val list = List(10) { it }
list eq "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
val list2 = List(10) { 0 }
list2 eq "[0, 0, 0, 0, 0, ,0, 0, 0, 0, 0]"
val list3 = List(10) {'a' + it }
list3 eq "[a, b, c, d, e, f, g, h, i, j]"
val list4 = List(10) {list3[it % 3] }
list4 eq "[a, b, c, a, b, c, a, b, c, a]"
}
List() and MutableList() are not constructors just functions named with uppercase letters to make them look like constructors
some other collection functions:
-
filter
-
filterNot
-
filterNotNull
-
partition
-
any
-
all
-
none
-
find - stops when the first matching result is found
-
lastOrNull
-
count
-
sum
-
sumOf
-
sorted
-
sortedDescending
-
sortedBy
-
sortedByDescending
-
take
-
takeWhile
-
takeLast
-
takeLastWhile
-
drop
-
dropWhile
-
dropLast
-
dropLastWhile
Here destructuring is used with a custom function:
package operationsoncollections
import atomictest.eq
fun createPair() = Pair(1, "one")
fun main() {
val (i, s) = createPair()
i eq 1
s eq "one"
}
Using sumOf and sortedBy
package operationsoncollections
import atomictest.eq
data class Product(
val description: String,
val price: Double
)
fun main() {
val products = listOf(
Product("bread", 2.0),
Product("wine", 5.0)
)
products.sumOf { it.price } eq 7.0
products.sortedByDescending { it.price } eq
"[Product(description=wine, price=5.0)," +
" Product(description=bread, price=2.0)]"
}
Example using takeLastWhile and dropWhile:
import atomictest.eq
fun main() {
val list = listOf('a', 'b', 'c', 'X', 'Y')
list.takeLast(3) eq listOf('c', 'X', 'Y')
list.takeLastWhile { it.isUpperCase() } eq
"[X, Y]"
list.drop(1) eq "[b, c, X, Y]"
list.dropWhile { it.isLowerCase() } eq
"[X, Y]"
}
When applying filter and map to a set the functions return a List
Member References
Member references for functions, properties, and constructors can replace trivial lambdas that simply call the corresponding function, property, or constructor.
Example member reference
package memberreferences1
import atomictest.eq
data class Message(
val sender: String,
val text: String,
val isRead: Boolean
)
fun main() {
val messages = listOf(
Message("Kitty", "Hey", true)
Message("Kitty", "Where are you?", false))
val unread = messages.filterNot(Message::isRead)
unread.size eq 1
unread.single().text eq "Where are you?"
}
Property references are useful when specifying a non-trivial sort order:
import memberreferences.Message
import atomictest.eq
fun main() {
val messages = listOf(
Message("Kitty", "Hey!", true),
Message("Kitty", "Where are you?", false),
Message("Boss", "Meeting today", false)
)
messages.sortedWith(compareBy(
Message::isRead, Message::sender)) eq
listOf(Message("Boss", "Meeting today", false),
Message("Kitty", "Where are you?", false),
Message("Kitty", "Hey!", true))
}
sortedWith() sorts a list using a comparator. Comparator’s are objects that compare two elements. Its args are predicates.
compareBy() with 1 arg is equivalent to sortedBy()
In Kotlin you can’t pass a function where a function type is expected, but you can pass a reference to that function:
package memberreferences2
import atomictest.eq
data class Message(
val sender: String,
val text: String,
val isRead: Boolean,
val attachments: List<Attachment>
)
data class Attachment(
val type: String,
val name: String
)
fun Message.isImportant(): Boolean =
text.contains("Salary increase") ||
attachments.any {
it.type =="image" &&
it.name.contains("cat")
}
fun main() {
val messages = listOf(
Message("Boss", "Let's discuss goals " +
"for next year", false,
listOf(Attachment("image", "cute cats"))
)
)
messages.any(Message::isImportant) eq true
}
References are not limited to member functions like in the above example.
If you have a top-level function taking Message as its only parameter, you can pass it as a reference. When you create a reference to a top-level function , there’s no class name:
package memberreferences2
import atomictest.eq
fun ignore(message: Message) =
!message.isImportant() &&
message.sender in setOf("Boss", "Mom")
fun main() {
val text = "Let's discuss goals for next year"
val msgs = listOf(
Message("Boss", text, false, listOf()),
Message("Boss", text, false listOf(
Attachment("image", "cute cats"))
)
)
msgs.filter(::ignore).size eq 1
msgs.filterNot(::ignore).size eq 1
}
You can create a reference to a constructor using the class name:
package memberreferences3
import atomictest.eq
data class Student(
val id: Int,
val name: String
)
fun main() {
val names = listOf("Alice", "Bob")
val students = names.mapIndexed { index, name ->
Student(index, name)
}
students eq listOf(Students(0, "Alice"),
Student(1, "Bob"))
names.mapIndexed(::Student) eq students
}
Higher-Order Functions
A language supports higher-orer functions if its functions can accept other funcs as args and produce functions as return values.
Storing a lambda in a reference
package higheroderfunctions
import atomictest.eq
val isPlus: (Int) -> Boolean = { it > 0 }
fun main() {
listOf(1, 2, -3).any(isPlus) eq true
}
Type annotations for functions follows:
(Arg1, Arg2, ...) -> Return Type
Calling a function through a reference:
package higherorderfunctions
import atomictest.eq
val helloWorld: () -> String =
{ "Hello, world!" }
val sum: (Int, Int) -> Int =
{ x, y -> x + y }
fun main() {
helloWorld() eq "Hello, world!"
sum(1, 2) eq 3
}
When a function accepts a function parameter you can pass either a function reference or a lambda.
package higherorderfunctions
import atomictest.eq
fun <T> List<T>.any(
predicate: (T) -> Boolean
): Boolean {
for (element in this) {
if predicate(element) {
return true
}
return false
}
}
fun main() {
val ints = listOf(1, 2, -3)
ints.any { it > 0 } eq true
val strings = listOf("abc", " ")
strings.any { it.isBlank() } eq true
strings.any(String::isNotBlank) eq true
}
Another example:
import atomictest.eq
fun main() {
repeat(4) { trace("hi") }
trace eq "hi hi hi hi"
}
How repeat may be implemented:
package higherorderfunctions
import atomictest.eq
fun repeat(
times: Int,
action: (Int) -> Unit
) {
for (index in 0 until times) {
action(index)
}
}
fun main() {
repeat(3) { trace("$it") }
trace eq "0 1 2"
}
A function return type can be nullable:
import atomictest.eq
fun main() {
val transform: (String) -> Int? =
{ s: String -> s.toIntOrNull() }
transform("112") eq 112
tranasform("abc") eq null
}
The whole function type can also be nullable:
fun main() {
val mightBeNull: ((String) -> Int)? = null
if (mightBeNull != null) {
mightBeNull("abc"
}
}
Manipulating Lists
zip() combines two lists by mimicking the behavior of the zipper on your jacket pairing adjacent List elements:
import atomictest.eq
fun main() {
val left = listOf("a", "b", "c", "d")
val right = listOf("q", "r", "s", "t")
left.zip(right) eq
"[(a, q), (b, r), (c, s), (d, t)]"
left.zip(0..4) eq
"[(a, 0), (b, 1), (c, 2), (d, 3)]"
(10..100).zip(right) eq
"[(10, q), (11, r), (12, s), (13, t)]"
}
Zipping left and right results in a List of Pairs.
You can also zip a list with a range like in the above example.
Zip can also perform operations on each Pair it creates:
package manipulatinglists
import atomictest.eq
data class Person(
val name: String,
val id: Int
)
fun main() {
val names = listOf("Bob", "Jill", "Jim")
val ids = listOf(0, 1, 2)
val names.zip(ids) { name, id ->
Person(name, id)
} eq "[Person(name=Bob, id=0), " +
"Person(name=Jill, id-1), " +
"Person(name=Jim, id=2)]"
}
To zip two adjacent elements from a single List use zipWithNext():
import atomictest.eq
fun main() {
val list = listOf('a', 'b', 'c', 'd')
list.zipWithNext() eq listOf(
Pair('a', 'b'),
Pair('b', 'c'),
Pair('c','d'))
list.zipWithNext { a, b -> "$a$b" } eq
"[ab, bc, cd]"
}
flatten() takes a List of Lists and flattens it into a List of single elements
import atomictest.eq
fun main() {
val list = listOf(
listOf(1, 2),
listOf(3, 4),
listOf(5, 6)
)
list.flatten() eq "[1, 2, 3, 4, 5, 6]"
}
Let’s produce all possible Pairs of a range of Ints
import atomictest.eq
fun main() {
val intRange = 1..3
intRange.map { a ->
intRange.map { b -> a to b }
} eq "[" +
"[(1, 1), (1, 2), (1, 3)], " +
"[(2, 1), (2, 2), (2, 3)], " +
"[(3, 1), (3, 2), (3, 3)]" +
"]"
intRange.map { a ->
intRange.map { b -> a to b }
}.flatten() eq "[" +
"(1, 1), (1, 2), (1, 3), " +
"(2, 1), (2, 2), (2, 3), " +
"(3, 1), (3, 2), (3, 3)" +
"]"
intRange.flatMap { a ->
intRange.map { b -> a to b }
} eq "[" +
"(1, 1), (1, 2), (1, 3), " +
"(2, 1), (2, 2), (2, 3), " +
"(3, 1), (3, 2), (3, 3)" +
"]"
}
Another example
package manipulatinglists
import kotlin.random.Random
import atomictest.*
enum class Suit {
Spade, Club, Heart, Diamond
}
enum class Rank(val faceValue: Int) {
Ace(1), Two(2), Three(3), Four(4), Five(5),
Six(6), Seven(7), Eight(8), Nine(9),
Ten(10), Jack(10), Queen(10), King(10)
}
class Card(val rank: Rank, val suit: Suit) {
override fun toString() {
return "$rank of ${suit}s"
}
}
val deck: List<Card> =
Suit.values().flatMap { suit ->
Rank.values().map { rank ->
Card(rank, suit)
}
}
fun main() {
val rand = Random(26)
repeat(7) {
trace("'${deck.random(rand)}'")
}
trace eq """
'Jack of Hearts' 'Four of Hearts'
'Five of Clubs' 'Seven of Clubs'
'Jack of Diamonds' 'Ten of Spades'
'Seven of Spades'
"""
}
Building Maps
Creating a repeatable set of data:
package buildingmaps
data class Person(
val name: String,
val age: Int
)
val names = listOf("Alice", "Arthricia", "Bob",
"Bill", "Birdperson", "Charlie",
"Crocubot", "Franz", "Revolio")
val ages = listOf(1, 2, 3,
3, 4, 5,
4, 2, 6)
fun people(): List<Person> =
names.zip(ages) { name, age ->
Person(name, age)
}
fun main() {
val map: Map<Int, List<Person>> =
people().groupBy(Person::age)
map[2] eq listOf(Person("Arthricia", 2))
map[4] eq listOf(Person("Birdperson", 4),
Person("Crocubot", 4)
}
groupBy in the above example creates a map of ages to Person objects.
Example using associateWith():
import buildingmaps.*
import atomictest.eq
fun main() {
val map: Map<Person, String> =
people().associateWith {it.name }
map[Person("Alice", 1)] eq "Alice"
}
associateBy is used in the same way but the lambda specifies the key not value
associateBy() must be used with a unique selection Key and returns a Map that paires each unique key to a single element selected by that key.
Example using getOrElse():
import atomictest.eq
fun main() {
val map = mapOf(1 to "one", 2 to "two")
map.getOrElse(0) { "zero" } eq "zero"
val mutableMap = map.toMutableMap()
mutableMap.getOrPut(0) { "zero" } eq "zero"
mutableMap eq "{1=one, 2=two, 0=zero}"
}
We can transform a Map using map(), mapKeys(), or mapValues():
import atomictest.eq
fun main() {
val even = mapOf(2 to "two", 4 to "four")
even.map {
"${it.key}=${it.value}"
} eq listOf("2=two", "4=four")
even.map { (key, value) ->
"$key=$value"
} eq listOf("2=two", "4=four")
even.mapKeys { (num, _) -> -num }
.mapValues { (_, str) -> "minus $str" } eq
mapOf(-2 to "minus two", -4 to "minus four")
even.map { (key, value) ->
-key to "minus $value"
}.toMap() eq
mapOf(-2 to "minus two", -4 to "minus four")
}
In the above example:
-
map() returns a list
-
mapKeys() and mapValues() return a Map with all keys or values transformed accordingly.
You can also use any() or all() with Maps.
You can also use maxByOrNull() which finds the maximum entry given the criteria
Sequences
A kotlin Sequence is like a List, but you can only iterate through a Sequence. You cannot index into a Sequence.
Sequences aka streams in other languages.
Operations on Lists are performed eagerly (they happen right away). The first operation must finish before the next operation begins.
For long Lists this may be suboptimal.
In lazy evaluation a result is only computed when needed.
If the final result of a calculation is found before processing the last element no further elements are processed.
Use asSequence() to convert List to Sequence. All List operations except indexing are available for Sequences.
package sequences
import atomictest.*
fun Int.isEven(): Boolean {
trace("$this.isEven()")
return this % 2 == 0
}
fun Int.square(): Int {
trace("$this.square()")
return this * this
}
fun Int.lessThanTen(): Boolean {
trace("$this.lessThanTen()")
return this < 10
}
fun main() {
val list = listOf(1, 2, 3, 4)
trace(">>> List:")
trace(
list
.filter(Int::isEven)
.map(Int::square)
.any(Int::lessThanTen)
)
trace(">>> Sequence:")
trace(
list.asSequence()
.filter(Int::isEven)
.map(Int::square)
.any(Int::lessThanTen)
)
trace eq """
>>> List:
1.isEven()
2.isEven()
3.isEven()
4.isEven()
2.square()
4.square()
4.lessThanTen()
true
>>> Sequence:
1.isEven()
2.isEven()
2.square()
4.lessThanTen()
true
}
Calling filter() or map() on a Sequence produces another Sequence.
Nothing happens until you ask for a result from a calculation:
import atomictest.eq
import sequences.*
fun main() {
val r = listOf(1, 2, 3, 4)
.asSequence()
.filter(Int::isEven)
.map(Int::square)
r.toString().substringBefore("@") eq
"kotlin.sequences.TransformingSequence"
}
We would need to use a Terminal Sequence operation to get a result. This includes any() or toList().
A terminal operation runs all stored operations in the process:
import sequences.*
import atomictest.*
fun main() {
val list = listOf(1, 2, 3, 4)
trace(
list.asSequence()
.filter(Int::isEven)
.map(Int::square)
.toList()
)
trace eq """
1.isEven()
2.isEven()
2.Square()
3.isEven()
4.isEven()
4.square()
[4, 16]
"""
}
Sequences can call operations in any order resulting in lazy evaluation.
generateSequence() creates a sequence of infinite numbers using an initial number and a lambda:
import atomictest.eq
fun main() {
val naturalNumbers =
generateSequence(1) { it + 1 }
naturalNumbers.take(3).toList() eq
listOf(1, 2, 3)
naturalNumbers.take(10).sum() eq 55
}
Another example using generateSequence() without the first parameter:
import atomictest.*
fun main() {
val items = mutableListOf(
"first", "second", "third",
"XXX", "4th"
)
val seq = generateSequence {
items.removeAt(0).takeIf { it != "XXX" }
}
seq.toList() eq ["first, second, third]"
capture {
seq.toList()
} eq "IllegalState Exception: This " +
"sequence can only be consumed once"
}
To make multiple passes through a sequence convert it to some type of Collection
Example implementation of takeIf() that can work with any type
package sequences
import atomictest.eq
fun <T> T.takeIf(
predicate: (T) -> Boolean
): T? {
return if (predicate(this)) this else null
}
fun main() {
"abc".takeIf { it != "XXX" } eq "abc"
"XXX".takeIf { it != "XXX" } eq null
}
Local Functions
You can define functions within other functions
Named functions defined within other functions are called local functions.
Eample:
import atomictest.eq
fun main() {
val logMsg = StringBuilder()
fun log(message: String) =
logMsg.appendLine(message)
log("Starting computation")
val x = 42
log("Computation result: $x")
logMsg.toString() eq """
Starting Computation
Computation result: 42
"""
}
Local functions close over their surrounding environment
You can create local extension functions:
import atomictest.eq
fun main() {
fun String.exclaim() = "$this!"
"Hello".exclaim() eq "Hello!"
}
Some setup for future examples:
package localfunctions
class Session(
val title: String,
val speaker: String
}
val sessions = listOf(
Session("Kotlin Coroutines", "Roman Elizarov"))
val favoriteSpeakers = setOf("Roman Elizarov")
You can refer to local function using a function reference:
import localfunctions.*
import atomictest.eq
fun main() {
fun interesting(session: Session): Boolean {
if (session.title.contains("Kotlin") &&
session.speaker in favoriteSpeakers) {
return true
}
// ... more checks
return false
}
sessions.any(::interesting) eq true
}
Anonymous functions are defined within other functions but have no name.
Example using an anonymous function:
import localfunctions.*
import atomictest.eq
fun main() {
sessions.any(
fun(session: Session): Boolean {
if (session.title.contains("Kotlin") &&
session.speaker in favoriteSpeakers) {
return true
}
// ... more checks
return false
}
) eq true
}
Below forEach acts upon a lambda containing a return:
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4, 5)
val value = 3
var result = ""
list.forEach {
result += "$it"
if (it == value) {
result eq "123"
return
}
}
result eq "Never gets here" // [1]
}
A return expression exits a function defined using fun not a lambda. This means [1] is never called.
To return only from a lambda and not from the surrounding function use a labeled return
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4, 5)
val value = 3
var result = ""
list.forEach {
result += "$it"
if (it == value) return@forEach
}
return eq "12345"
}
Above the label is the name of the function that called the lambda.
Create a custom label for a lambda like so:
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4, 5)
val value = 3
var result = ""
list.forEach tag@{
result += "$it"
if (it == value) return@tag
}
}
You can store a lambda or anonymous function in a var or val.
To store a local function use a function reference:
package localfunctions
import atomictest.eq
// Returning an Anonymous function
fun first(): (Int) -> Int {
val func = fun(i: Int) = i + 1
func(1) eq 2
return func
}
//Returning a lambda
fun second(): (String) -> String {
val func2 = { s: String -> "$s!" }
func2("abc") eq "abc!"
return func2
}
//Returning a reference to a local function
fun third(): () -> String {
fun greet() = "Hi!"
return ::greet
}
//Returning a reference to a anyonymous function
//Using an expression body
fun fourth() = fun() = "Hi!"
//Returning a lambda using an expression body
fun fifth() = { "Hi!" }
fun main() {
val funRef1: (Int) -> Int = first()
val funRef2: (String) -> String = second()
val funRef3: () -> String = third()
val funRef4: () -> String = fourth()
val funRef5: () -> String = fifth()
funRef1(42) eq 43
//Alt
first()(42) eq 43
funRef2("abc") eq "abc!"
//Alt
second()("abc") eq "abc!"
funRef3() eq "Hi!"
//Alt
third()() eq "Hi!"
funRef4() eq "Hi!"
//...same
funRef5() eq "Hi!"
//..same
}
Folding Lists
fold() combines all elements of a list, in order, to generate a single result
Example;
import atomictest.eq
fun main() {
val list = listOf(1, 10, 100, 1000)
list.fold(0) { sum, n ->
sum + n
} eq 1111
}
0 here is the initial value
foldRight processes elements starting from rightToLeft()
import atomictest.eq
fun main() {
val list = listOf('a', 'b', 'c', 'd')
list.fold("*") { acc, elem ->
"($acc) + elem"
} eq ((((*) + a) + b) + c) + d"
list.foldRight("*") { elem, acc ->
"$elem + ($acc)"
} eq "a + (b + (c + (d + (*))))"
}
Use reduce() or reduceRight() like fold() but the initial value is taken from the list
runningFold() and runningReduce() produce a List of all intermediate steps of the process:
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3, 4)
list.fold(0) { sum, n ->
sum + n
} eq 10
list.runningFold(0) { sum, n ->
sum + n
} eq "[0, 1, 3, 6, 10]"
list.reduce { sum, n -> sum + n } eq 10
list.runningReduce { sum, n ->
sum + n
} eq "[1, 3, 6, 10]
}
Recursion
Recursion is a technique of calling a function within that same function.
Tail recursion is an optimization that can be explicitly applied to some recursive functions.
A recursive function uses the result of the previous recursive call
package recursion
import atomictest.eq
fun factorial(n: Long): Long {
if (n <= 1) return 1
return n * factorial(n - 1)
}
fun main() {
factorial(5) eq 120
}
Recursive functions are expensive because of a large call stack.
Using tailrec successfully is based on if :
- recursion is the final operation - no extra calculation before a result is returned
Example for sum
package tailrecursion
import atomictest.eq
private tailrec fun sum(
n: Long,
accumulator: Long
): Long =
if (n == 0L) accumulator
else sum(n - 1, accumulator + n)
fun sum(n: Long) = sum(n, 0)
fun main() {
sum(2) eq 3
}
Example of fibonacci sequence before and after tailrec
package slowfibonacci
import atomictest.eq
fun fibonacci(n: Long): Long {
return when(n) {
0L -> 0
1L -> 1
else ->
fibonacci(n - 1) + fibonacci(n - 2)
}
}
fun main() {
fibonacci(0L) eq 0L
fibonacci(22L) eq 17711L
}
package recursion
import atomictest.eq
fun fibonacci(n: Int): Long {
tailrec fun fibonacci(
n: Int,
current: Long,
next: Long
): Long {
if (n == 0) return current
return fibonacci(n - 1, next, current + next)
}
return fibonacci(n, 0L, 1L)
}
fun main() {
(0..8).map { fibonacci(it) } eq
"[0L, 1L, 1L, 2L, 3L, 5L, 8L, 13L, 21L]"
fibonacci(22) eq 17711L
}