Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Fun in Kotlin

Functional Fun in Kotlin

Kotlin is great language to do modern functional programming, and in my opinion perhaps the best language to do modern mainstream (hardcore) functional programming. With the power of Kotlin DSLs we can make functional programming idiomatic, simple and elegant. This talk takes us through the different techniques that we can apply in Kotlin to achieve modern, and elegant functional patterns.

Simon Vergauwen

February 04, 2023
Tweet

More Decks by Simon Vergauwen

Other Decks in Programming

Transcript

  1. Functional Fun in
    Kotlin
    47deg.com

    View Slide

  2. Who am I?

    View Slide

  3. Functional Fun in Kotlin
    Dependency Injection
    Side effects
    Typed Errors

    View Slide

  4. Dependency Injection
    interface Database {
    fun query(sql: String, vararg params: String): ResultRow
    }
    interface Logger {
    fun log(message: String): Unit
    fun error(throwable: Throwable, message: String): Unit
    }

    View Slide

  5. Dependency Injection
    fun fetchUser(id: Long, db: Database, logger: Logger): User {
    val user = db.query("SELECT * FROM users WHERE id = $1", id).toUser()
    logger.log("Query result: $user")
    return user
    }

    View Slide

  6. Dependency Injection
    fun fetchUser(id: Long, db: Database, logger: Logger): User {
    val user = db.query("SELECT * FROM users WHERE id = $1", id).toUser()
    logger.log("Query result: $user")
    return user
    }
    fun fetchAll(ids: List, db: Database, logger: Logger): List =
    ids.map { id -> fetchUser(id, db, logger) }

    View Slide

  7. Dependency Injection
    fun Context.fetchUser(id: Long): User

    View Slide

  8. Dependency Injection
    fun Context.fetchUser(id: Long): User
    where Context : Database, Context : Logger

    View Slide

  9. Dependency Injection
    fun Context.fetchUser(id: Long): User
    where Context : Database, Context : Logger {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }

    View Slide

  10. Dependency Injection
    fun Context.fetchUser(id: Long): User
    where Context : Database, Context : Logger {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }
    class Module(db: Database, logger: Logger): Database by db, Logger by logger

    View Slide

  11. Dependency Injection
    fun Context.fetchUser(id: Long): User
    where Context : Database, Context : Logger {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }
    class Module(db: Database, logger: Logger): Database by db, Logger by logger
    fun Context.fetchAll(ids: List): List
    where Context : Database, Context : Logger =
    ids.map { id -> fetchUser(id) }

    View Slide

  12. Dependency Injection
    context(Database, Logger)
    fun fetchUser(id: Long): User

    View Slide

  13. Dependency Injection
    context(Database, Logger)
    fun fetchUser(id: Long): User {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }

    View Slide

  14. Dependency Injection
    context(Database, Logger)
    fun fetchUser(id: Long): User {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }
    context(Database, Logger)
    fun fetchAll(ids: List): List =
    ids.map { id -> fetchUser(id) }

    View Slide

  15. Side effects
    Compile time tracked effects
    Composable
    Reason about code that
    performs side effects

    View Slide

  16. Side effects
    interface Database {
    fun query(sql: String, vararg params: String): ResultRow
    }
    interface Logger {
    fun log(message: String): Unit
    fun error(throwable: Throwable, message: String): Unit
    }

    View Slide

  17. Side effects
    interface Database {
    suspend fun query(sql: String, vararg params: String): ResultRow
    }
    interface Logger {
    suspend fun log(message: String): Unit
    suspend fun error(throwable: Throwable, message: String): Unit
    }

    View Slide

  18. Side effects
    context(Database, Logger)
    fun fetchUser(id: Long): User {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }

    View Slide

  19. Side effects
    context(Database, Logger)
    fun fetchUser(id: Long): User {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }
    Suspend function 'query' should be called only from a coroutine or another suspend function
    Suspend function 'log' should be called only from a coroutine or another suspend function

    View Slide

  20. Side effects
    context(Database, Logger)
    suspend fun fetchUser(id: Long): User {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }

    View Slide

  21. Suspend - Continuation Passing Style
    interface Continuation {
    val context: CoroutineContext
    fun resume(value: T)
    fun resumeWithException(exception: Throwable)
    }

    View Slide

  22. Suspend - Continuation Passing Style
    context(Database, Logger)
    fun fetchUser(id: Long, cont: Continuation): Unit {
    query("SELECT * FROM users WHERE id = $1", id, Continuation(cont.context) { res ->
    })
    }

    View Slide

  23. Suspend - Continuation Passing Style
    context(Database, Logger)
    fun fetchUser(id: Long, cont: Continuation): Unit {
    query("SELECT * FROM users WHERE id = $1", id, Continuation(cont.context) { res ->
    res.fold({ resultRow ->
    val user = resultRow.toUser()
    log2("Query result: $user", Continuation(cont.context) { res ->
    })
    }, cont::resumeWithException)
    })
    }

    View Slide

  24. Suspend - Continuation Passing Style
    context(Database, Logger)
    fun fetchUser(id: Long, cont: Continuation): Unit {
    query("SELECT * FROM users WHERE id = $1", id, Continuation(cont.context) { res ->
    res.fold({ resultRow ->
    val user = resultRow.toUser()
    log2("Query result: $user", Continuation(cont.context) { res ->
    res.fold({ cont.resume(user) }, cont::resumeWithException)
    })
    }, cont::resumeWithException)
    })
    }

    View Slide

  25. Suspend - Continuation Passing Style
    context(Database, Logger)
    fun fetchUser(id: Long, cont: Continuation): Unit {
    query("SELECT * FROM users WHERE id = $1", id, Continuation(cont.context) { res ->
    res.fold({ resultRow ->
    val user = resultRow.toUser()
    log2("Query result: $user", Continuation(cont.context) { res ->
    res.fold({ cont.resume(user) }, cont::resumeWithException)
    })
    }, cont::resumeWithException)
    })
    }

    View Slide

  26. Side effects
    context(Database, Logger)
    suspend fun fetchUser(id: Long): User {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }

    View Slide

  27. Side effects
    context(Database, Logger)
    suspend fun fetchUser(id: Long): User {
    val user = query("SELECT * FROM users WHERE id = $1", id).toUser()
    log("Query result: $user")
    return user
    }
    context(Database, Logger)
    suspend fun fetchAll(ids: List): List =
    ids.map { id -> fetchUser(id) }

    View Slide

  28. Compile time tracking of expected
    errors
    Explicit in type signature
    Typed Errors

    View Slide

  29. Typed Errors
    context(Database, Logger)
    suspend fun fetchUser(id: Long): User?

    View Slide

  30. Typed Errors
    data class UserAlreadyExists(val error: PSQLException)
    context(Database, Logger)
    suspend fun insertUser(name: String, email: String): User

    View Slide

  31. Typed Errors
    data class UserAlreadyExists(
    val name: String,
    val email: String,
    val error: PSQLException
    )
    context(Database, Logger)
    suspend fun insertUser(name: String, email: String): Either

    View Slide

  32. Typed Errors
    data class UserAlreadyExists(
    val name: String,
    val email: String,
    val error: PSQLException
    )
    context(Database, Logger, Raise)
    suspend fun insertUser(name: String, email: String): User

    View Slide

  33. Typed Errors
    context(Database, Logger, Raise)
    suspend fun insertUser(name: String, email: String): User =
    catch({
    query("""
    INSERT INTO users
    VALUES ($1, $2)
    RETURNING user_id;
    """.trimIndent()).toUser()
    }) { error: PSQLException ->
    if (error.isUniqueViolation()) raise(UserAlreadyExists(name, email, error))
    else throw error
    }

    View Slide

  34. Monadic DSLs
    context(Database, Logger, Raise)
    suspend fun insertUser(name: String, email: String): User =
    catch({
    query("""
    INSERT INTO users
    VALUES ($1, $2)
    RETURNING user_id;
    """.trimIndent()).toUser()
    }) { error: PSQLException ->
    if (error.isUniqueViolation()) raise(UserAlreadyExists(name, email, error))
    else throw error
    }

    View Slide

  35. Arrow
    DSL based functional
    programming
    Idiomatic Kotlin syntax
    Kotlin
    Multiplatform
    ready

    View Slide

  36. Saga pattern
    context(SagaScope)
    suspend fun sagaOperation(): Result =
    saga({ action() }) { compensation() }

    View Slide

  37. Resource Safety
    context(ResourceScope, Logger)
    suspend fun custom(): CustomResource =
    install({ CustomResource. acquire() }) { res, exitCase ->
    log("Releasing $res due to $exitCase")
    res.release()
    }
    context(ResourceScope)
    suspend fun dataSource(config: HikariConfig): HikariDataSource =
    autoCloseable { HikariDataSource(config) }

    View Slide

  38. Thanks!

    View Slide