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

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