Kotlin Extension Functions
The power of being the author of a published library!
Agenda
- Basics
- Standard Extension Functions
- Defining your own Extension Functions
- Authoring the CompletableFuture API
- Questions?
What are Extension Functions?
- Provide the ability to extend a class without modifying it.
- Better alternative to creating * Util classes.
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' corresponds to MutableList
this[index1] = this[index2]
this[index2] = tmp
}
val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'l'EF are resolved statically
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
// prints "c"
printFoo(D())- EF are dispatched statically instead of virtual
- The EF being called is determined by the type of the expression not the actual type at runtime.
EF are resolved statically
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
fun run() {
C().foo() // prints member
}- When having both a member and EF, the member function always wins.
Nullable Receiver Type
fun Any?.toString(): String {
if (this == null) return "null"
// after the null check, 'this' is autocast to a non-null type, so the toString() below
// resolves to the member function of the Any class
return toString()
}Standard Extension Functions
- run
- let
- apply
- also
- takeIf
- takeUnless
- to
run
-
Calls the specified function block with this value as its receiver and returns its result.
inline fun <T, R> T.run(block: T.() -> R): R
// usage
"/var/output/foo".run {
File(this) // note we are using this, since we are using a receiver type T.()
}let
-
Calls the specified function block with this value as its argument and returns its result.
public inline fun <T , R > T.let
(block: (T) -> R ): R = block (this)
// usage
"/var/output/foo".let {
File(it)
}apply
-
Calls the specified function block with this value as its receiver and returns this value.
public inline fun <T> T.apply(block: T .() -> Unit): T {
block(); return this
}
// usage
val person = Person().apply {
firstName = "Victor"
lastName = "Reventos"
}also
-
Calls the specified function block with this value as its argument and returns this value.
public inline fun <T> T.also(block: (T) -> Unit): T {
block(this); return this
}
// usage
val person = Person().also {
it.firstName = "Victor"
it.lastName = "Reventos"
}to
-
Creates a tuple of type Pair from this and that
infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
// usage
val m = mapOf ("foo" to "creating a" , "bar" to "map")Standard EF selection

Credits for this image: Elye Medium Post
Defining your own EFs
- Use cases
- Add methods to classes you don't control
- Make 3rd party libraries more Kotlin like
Authoring the CompletableFuture API
- Motivation
-
The CompletableFuture API introduces unnecessary vocabulary, which confuses the developer
- thenApply -> map
- thenCompose -> flatMap
- In the end a Future is just another Monad.
- Make it more Kotlin like
- Heavily inspired by Scala's Future API
- Don't reinvent the wheel, by having everybody switch to a new future type
-
The CompletableFuture API introduces unnecessary vocabulary, which confuses the developer
Creating a Future
inline fun <A> Future(executor: Executor = ForkJoinExecutor,
crossinline block: () -> A): CompletableFuture<A> =
CompletableFuture.supplyAsync(Supplier { block() }, executor)
// New API
val future: CompletableFuture<Int> = Future { 10 }
// ForkJoinExecutor its just an alias ForkJoinPool.commonPool()
val futureOnForkJoin = Future(ForkJoinExecutor) { 10 }
val myExecutor = Executors.newSingleThreadExecutor()
val futureWithCustomExecutor = Future(myExecutor) { 10 }
// Old API
val future: CompletableFuture<Int> = CompletableFuture.supplyAsync { 10 }
// ForkJoinExecutor its just an alias ForkJoinPool.commonPool()
val futureOnForkJoin = CompletableFuture.supplyAsync(Supplier { 10 }, ForkJoinExecutor)
val myExecutor = Executors.newSingleThreadExecutor()
val futureWithCustomExecutor = CompletableFuture.supplyAsync(Supplier { 10 }, myExecutor)map
inline fun <A, B> CompletableFuture<A>.map(executor: Executor = ForkJoinExecutor,
crossinline f: (A) -> B): CompletableFuture<B> =
thenApplyAsync(Function { f(it) }, executor)
// New API
val future: CompletableFuture<String> = Future { 10 }
.map { "Hello user with id: $it" }
// Old API
val future: CompletableFuture<String> = Future { 10 }
.thenApplyAsync(Function { userId -> "Hello user with id: $userId" },
ForkJoinExecutor)- Map allows you to transform the success of this future into another future.
flatMap
inline fun <A, B> CompletableFuture<A>.flatMap(executor: Executor = ForkJoinExecutor,
crossinline f: (A) -> CompletableFuture<B>): CompletableFuture<B> =
thenComposeAsync(Function { f(it) }, executor)
// New API
// Fetching the posts depends on fetching the User
val posts = fetchUser(1).flatMap { fetchPosts(it) }
// Fetching both the user and the posts and then combining them into one
val userPosts = fetchUser(1).flatMap { user ->
fetchPosts(user).map { UserPosts(user, it) }
}
// Old API
val posts: CompletableFuture<List<Post>> = fetchUser(1)
.thenComposeAsync(Function { fetchPosts(it) }, ForkJoinExecutor)
val userPosts: CompletableFuture<UserPosts> = fetchUser(1)
.thenComposeAsync(Function { user ->
fetchPosts(user).thenApplyAsync(Function { posts ->
UserPosts(user, posts)
}, ForkJoinExecutor)
}, ForkJoinExecutor)-
flatMap allows you to do sequential composition. Creating a new future dependent on another one.
filter
inline fun <A> CompletableFuture<A>.filter(executor: Executor = ForkJoinExecutor,
crossinline predicate: (A) -> Boolean): CompletableFuture<A> =
map(executor) {
if (predicate(it)) it
else throw NoSuchElementException("CompletableFuture.filter predicate is not satisfied")
}
// New API
val future = Future { 10 }
// This future will succeed
val success = future.filter { it % 2 == 0 }
// This future will succeed
val success = future.filter { it % 2 == 0 }
// This future will throw NoSuchElementException
val failed = future.filter { it % 3 == 0 }
// Fetching both the user and the posts and then combining them into one
val userPosts = fetchUser(1).flatMap { user ->
fetchPosts(user).map { UserPosts(user, it) }
}
// Old API
// **** There is no filter method in CompletableFuture-
filter will convert this future to a failed future if it doesn't match the predicate.
The end result
- map
- flatMap
-
flatten
-
filter
-
zip
- recover
- recoverWith
- fallbackTo
- mapError
- onSuccess
- onFailure
- onComplete
kotlin-futures: A collection of extension functions to make the JVM Future, CompletableFuture, ListenableFuture API more functional and Kotlin like.
Questions?
Kotlin Extension Fucntions
By Victor J. Reventos
Kotlin Extension Fucntions
- 81