$30 off During Our Annual Pro Sale. View Details »

Kotlin Extension Functions

Kotlin Extension Functions

Christoph Leiter

April 18, 2017
Tweet

More Decks by Christoph Leiter

Other Decks in Technology

Transcript

  1. Extension Functions
    Christoph Leiter
    Kotlin Vienna, April 18th 2017

    View Slide

  2. Agenda
    1 Basics
    2 General Extension Functions
    3 Handy Extension Functions in stdlib
    4 Defining your own Extension Functions
    5 Summary
    2

    View Slide

  3. Basics

    View Slide

  4. What are EF?
    EFs allow to add new functionality to a class without
    modifying it
    Better alternative to creating *Util classes
    They are statically resolved, there’s no patching of the original
    classes
    Works with final classes, e.g. String
    Kotlin also has extension properties which are similar
    4

    View Slide

  5. Declaring an EF
    Extension function:
    fun SomeType. extensionFunction
    (x: String ): Int { ... }
    Inside the fun you can reference the instance with this
    Even works with nullable types (this will be null)
    Extension property:
    val SomeType.empty: Boolean
    get() = size == 0
    5

    View Slide

  6. Interoperability
    You can call EF from Java as well but you need to call the
    static method directly
    If your EFs are in a file called foo.kt Kotlin will generate the
    class FooKt with static methods
    First argument is the receiver object (this)
    6

    View Slide

  7. Interoperability
    You can call EF from Java as well but you need to call the
    static method directly
    If your EFs are in a file called foo.kt Kotlin will generate the
    class FooKt with static methods
    First argument is the receiver object (this)
    Example extensions.kt:
    fun Int.toBigInteger () =
    BigInteger.valueOf(this.toLong ())
    Java:
    ExtensionsKt.toBigInteger (10);
    6

    View Slide

  8. General Extension Functions

    View Slide

  9. General Extension Functions
    There are several handy extension functions defined in stdlib
    (Standard.kt) which work on Any:
    run
    let
    apply
    also
    takeIf
    takeUnless
    to
    8

    View Slide

  10. let
    Calls the specified function block with this value as its argument
    and returns its result.
    public inline fun T.let
    (block: (T) -> R): R = block(this)
    9

    View Slide

  11. let
    Calls the specified function block with this value as its argument
    and returns its result.
    public inline fun T.let
    (block: (T) -> R): R = block(this)
    Example:
    class C {
    var d: D? = null
    fun f() {
    d?.let { it.g() }
    }
    }
    Use to capture variable if smart cast not possible due to mutability.
    9

    View Slide

  12. run
    Calls the specified function block with this value as its receiver
    and returns its result.
    public inline fun T.run
    (block: T.() -> R): R = block ()
    10

    View Slide

  13. run
    Calls the specified function block with this value as its receiver
    and returns its result.
    public inline fun T.run
    (block: T.() -> R): R = block ()
    Example:
    val x = y.run() {
    a = 1 // refers to y.a
    b = 2 // refers to y.b
    compute () // refers to y.compute ()
    }
    Use if you want to access multiple members of an instance without
    repeating yourself but you want to return something different.
    Might also be useful to execute multiple methods of an instance
    with a null check by using ?.run(..).
    10

    View Slide

  14. apply
    Calls the specified function block with this value as its receiver
    and returns this value.
    public inline fun T.apply
    (block: T.() -> Unit ): T
    { block (); return this }
    11

    View Slide

  15. apply
    Calls the specified function block with this value as its receiver
    and returns this value.
    public inline fun T.apply
    (block: T.() -> Unit ): T
    { block (); return this }
    Example:
    val bean = MyBean (). apply {
    a = 1
    b = 2
    c = true
    }
    Use to instantiate instances and set some values.
    11

    View Slide

  16. also
    Calls the specified function block with this value as its argument
    and returns this value.
    public inline fun T.also
    (block: (T) -> Unit ): T
    { block(this ); return this }
    12

    View Slide

  17. also
    Calls the specified function block with this value as its argument
    and returns this value.
    public inline fun T.also
    (block: (T) -> Unit ): T
    { block(this ); return this }
    Example:
    fun MyBean.copy () =
    MyBean (). also {
    it.a = a // it refers to new instance
    it.b = b // implicit this refers to outer
    }
    12

    View Slide

  18. to
    Creates a tuple of type Pair from this and that
    public infix fun A.to(that: B):
    Pair = Pair(this , that)
    Useful for creating map entries with less verbosity:
    val m = mapOf("foo" to 23, "bar" to 42)
    13

    View Slide

  19. takeIf
    Returns this value if it satisfies the given predicate or null, if it
    doesn’t.
    public inline fun T.takeIf
    (predicate: (T) -> Boolean ): T? =
    if (predicate(this )) this else null
    14

    View Slide

  20. takeIf
    Returns this value if it satisfies the given predicate or null, if it
    doesn’t.
    public inline fun T.takeIf
    (predicate: (T) -> Boolean ): T? =
    if (predicate(this )) this else null
    Example:
    val x = foo.takeIf { it.p() } ?: return true
    Like filter for a single value.
    14

    View Slide

  21. takeUnless
    Returns this value if it does not satisfy the given predicate or
    null, if it does.
    public inline fun T.takeUnless
    (predicate: (T) -> Boolean ): T? =
    if (! predicate(this )) this else null
    Same as a negated takeIf.
    15

    View Slide

  22. Handy Extension Functions in
    stdlib

    View Slide

  23. Array
    flatten: Returns a single list of all elements from all arrays in
    the given array.
    fun Array >. flatten ():
    List
    17

    View Slide

  24. Array
    flatten: Returns a single list of all elements from all arrays in
    the given array.
    fun Array >. flatten ():
    List
    unzip: Returns a pair of lists, where first list is built from the first
    values of each pair from this array, second list is built from the
    second values of each pair from this array.
    fun Array >. unzip ():
    Pair , List >
    17

    View Slide

  25. Iterable
    flatten: Returns a single list of all elements from all collections
    in the given collection.
    fun Iterable >. flatten ():
    List
    18

    View Slide

  26. Iterable
    flatten: Returns a single list of all elements from all collections
    in the given collection.
    fun Iterable >. flatten ():
    List
    unzip: Returns a pair of lists, where first list is built from the first
    values of each pair from this collection, second list is built from
    the second values of each pair from this collection.
    fun Iterable >. unzip ():
    Pair , List > {
    18

    View Slide

  27. List
    orEmpty: Returns this List if it’s not null and the empty list
    otherwise.
    inline fun List ?. orEmpty ():
    List = this ?: emptyList ()
    19

    View Slide

  28. List
    orEmpty: Returns this List if it’s not null and the empty list
    otherwise.
    inline fun List ?. orEmpty ():
    List = this ?: emptyList ()
    binarySearch: Searches this list or its range for the provided
    element using the binary search algorithm.
    fun > List . binarySearch (
    element: T?,
    fromIndex: Int = 0,
    toIndex: Int = size
    ): Int
    19

    View Slide

  29. Map
    getOrElse: Returns the value for the given key, or the result of
    the defaultValue function if there was no entry for the given key.
    inline fun Map . getOrElse(
    key: K,
    defaultValue: () -> V
    ): V = get(key) ?: defaultValue ()
    20

    View Slide

  30. Map
    getOrElse: Returns the value for the given key, or the result of
    the defaultValue function if there was no entry for the given key.
    inline fun Map . getOrElse(
    key: K,
    defaultValue: () -> V
    ): V = get(key) ?: defaultValue ()
    Alternative to using ?:
    val v = map.getOrElse(k) { myDefaultValue }
    val w = map[k] ?: myDefaultValue
    20

    View Slide

  31. Map II
    getOrPut: Returns the value for the given key. If the key is not
    found in the map, calls the defaultValue function, puts its result
    into the map under the given key and returns it.
    inline fun
    MutableMap . getOrPut(
    key: K,
    defaultValue: () -> V
    ): V
    21

    View Slide

  32. Map II
    getOrPut: Returns the value for the given key. If the key is not
    found in the map, calls the defaultValue function, puts its result
    into the map under the given key and returns it.
    inline fun
    MutableMap . getOrPut(
    key: K,
    defaultValue: () -> V
    ): V
    Easy way to get a value out of a cache and if it doesn’t exist
    compute it and put it in the cache
    val v = cache.getOrPut(key) {
    computeValue(key)
    }
    21

    View Slide

  33. Map III
    Methods to filter Maps based on Entrys, keys or values:
    inline fun Map . filter(
    predicate: (Map.Entry ) -> Boolean
    ): Map
    inline fun Map . filterKeys(
    predicate: (K) -> Boolean
    ): Map {
    inline fun Map . filterValues (
    predicate: (V) -> Boolean
    ): Map
    22

    View Slide

  34. Comparator
    thenBy: Creates a comparator comparing values after the primary
    comparator defined them equal. It uses the function to transform
    value to a Comparable instance for comparison.
    inline fun Comparator . thenBy(
    crossinline selector: (T) -> Comparable <*>?
    ): Comparator {
    23

    View Slide

  35. Comparator
    thenBy: Creates a comparator comparing values after the primary
    comparator defined them equal. It uses the function to transform
    value to a Comparable instance for comparison.
    inline fun Comparator . thenBy(
    crossinline selector: (T) -> Comparable <*>?
    ): Comparator {
    Allows chaining of multiple comparators
    list.sortedWith(
    compareBy
    { it.shoeSize }. thenBy { it.age }
    )
    23

    View Slide

  36. Lock
    withLock: Executes the given action under this lock and returns
    its value.
    inline fun Lock.withLock(
    action: () -> T
    ): T
    24

    View Slide

  37. Lock
    withLock: Executes the given action under this lock and returns
    its value.
    inline fun Lock.withLock(
    action: () -> T
    ): T
    Allows easier lock usage:
    lock.withLock () {
    doSomething ()
    }
    Also has read and write EFs for ReentrantReadWriteLock
    24

    View Slide

  38. Closeable
    use: Executes the given block function on this resource and then
    closes it down correctly whether an exception is thrown or not.
    inline fun T.use(
    block: (T) -> R
    ): R
    25

    View Slide

  39. Closeable
    use: Executes the given block function on this resource and then
    closes it down correctly whether an exception is thrown or not.
    inline fun T.use(
    block: (T) -> R
    ): R
    Works like try-with-resources in Java but is not a language
    feature
    stream.use {
    readFrom(it)
    }
    25

    View Slide

  40. Defining your own Extension
    Functions

    View Slide

  41. Use cases
    There are many good use cases to use EFs in your code:
    Add methods to classes which you don’t control
    Add methods to classes depending on the context
    toDTO in REST layer
    toJPA in storage layer
    Make 3rd party libraries more Kotlin like
    27

    View Slide

  42. DTO Converter
    class UsersController (
    private val userRepository : UserRepository
    ) {
    fun getUser(id: Long) =
    userRepository .find(id)?. toDTO ()
    }
    private fun User.toDTO (): UserDTO =
    UserDTO (). also {
    it.name = "$firstname $lastname"
    it.age = calculateAge (birthday)
    }
    28

    View Slide

  43. SQL like IN
    fun Any?. isIn(vararg objs: Any ?): Boolean =
    objs.any { it == this }
    29

    View Slide

  44. SQL like IN
    fun Any?. isIn(vararg objs: Any ?): Boolean =
    objs.any { it == this }
    Allows compact syntax for comparing a value to multiple
    possibilities. Avoid repetition of the value if it’s a complex
    expression.
    if (foo.bar (). buz (). isIn (23, 42, 404)) {
    doSomething ()
    }
    29

    View Slide

  45. MonetaryAmount
    JSR-354 defines a new MonetaryAmount. We can extend it to feel
    more like a native type with operator EFs:
    operator fun MonetaryAmount .plus(
    other: MonetaryAmount
    ): MonetaryAmount = this.add(other)
    operator fun MonetaryAmount .times(
    other: Number
    ): MonetaryAmount = this.multiply(other)
    30

    View Slide

  46. MonetaryAmount
    JSR-354 defines a new MonetaryAmount. We can extend it to feel
    more like a native type with operator EFs:
    operator fun MonetaryAmount .plus(
    other: MonetaryAmount
    ): MonetaryAmount = this.add(other)
    operator fun MonetaryAmount .times(
    other: Number
    ): MonetaryAmount = this.multiply(other)
    Allows use of arithmetic symbols:
    val a = Money.of(30, "EUR")
    val b = Money.of(50, "EUR")
    val c = a + b * 2
    30

    View Slide

  47. MonetaryAmount II
    You could even define something like
    val Number.EUR: MonetaryAmount
    get() = Money.of(this , "EUR")
    31

    View Slide

  48. MonetaryAmount II
    You could even define something like
    val Number.EUR: MonetaryAmount
    get() = Money.of(this , "EUR")
    Then you could write
    assertThat(calculation ()). isEqualTo (30. EUR)
    I wouldn’t recommend this for production code but for unit tests it
    might be handy.
    31

    View Slide

  49. Formatting for Remote Systems
    When communicating with remote systems it’s handy to have
    functions for data types to convert from/to remote types.
    fun LocalDate.format () =
    format( DateTimeFormatter .ISO_DATE)
    fun MonetaryAmount .convert (): BigDecimal =
    number.numberValue(BigDecimal :: class.java)
    .multiply(BigDecimal.valueOf (100))
    fun XMLGregorianCalendar .toLocalDate () =
    toGregorianCalendar ()
    . toZonedDateTime ()
    .toLocalDate ()
    Make sure to mark them private or internal to avoid polluting
    the namespace.
    32

    View Slide

  50. Redisson Lock
    inline fun RedissonClient .lock(
    lockName: String ,
    waitTime: Duration ,
    leaseTime: Duration ,
    action: (RLock) -> T
    ): LockResult {
    val lock = getLock(lockName)
    val success = lock.tryLock(
    waitTime.toMillis (),
    leaseTime.toMillis (),
    TimeUnit.MILLISECONDS)
    // check - try action - finally unlock
    }
    33

    View Slide

  51. Redisson Lock II
    Easy and consistent usage of locks, no need to repeat the
    try-finally block all the time
    Looks more like a language feature
    Allows usage of Duration instead of milliseconds as Long
    You can set default values for waitTime and leaseTime
    redisson.lock("my -lock") {
    doSomethingInLock ()
    }
    34

    View Slide

  52. EFs for Lambdas
    You can even define EFs on lambdas:
    fun (() -> Any). time (): Long {
    val start = System. currentTimeMillis ()
    this ()
    return System. currentTimeMillis () - start
    }
    val t = { computeSomething () }. time ()
    35

    View Slide

  53. EFs for Lambdas
    You can even define EFs on lambdas:
    fun (() -> Any). time (): Long {
    val start = System. currentTimeMillis ()
    this ()
    return System. currentTimeMillis () - start
    }
    val t = { computeSomething () }. time ()
    Haven’t found a good use case for that but it’s good to know
    nonetheless. :)
    35

    View Slide

  54. Summary

    View Slide

  55. Summary
    EFs are syntactic sugar but really helpful
    Help to make your code more concise
    Can be used to “extend” the language (e.g. use is a language
    feature in Java)
    Even though you can define the same EFs multiple times it’s
    best not to pollute the namespace for common types
    37

    View Slide

  56. End
    Thanks for your attention
    Questions?
    Kotlin Vienna Meetup
    https://www.meetup.com/Kotlin-Vienna/
    38

    View Slide