Type to search…

Function type

Les funcions es poden tractar com un tipus de dades.

Introducció

A Funció vas aprendre a declarar funcions amb la paraula clau fun.

Com que les funcions també són tipus de dades les pots desar en variables, passar com a paràmetres a altres funcions, retornar-les com a valor de retorn, etc.

Referenciar una funció

A continuació tens la funció hello:

kotlin
fun main() {

    fun hello(): String {
        return "Hello"
    }

    require(hello() == "Hello")
}

En lloc de cridar la funció hello directament, la pots desar en una variable perquè una funció és un objecte com pot ser un 3 o "Hello".

Et pots referir a la funció hello com un valor amb l’operador de referència de funció (::):

kotlin
fun main() {

    fun hello(): String {
        return "Hello"
    }

    val hi = ::hello

    require(hi() == "Hello")
}

La constant hi té tipus () -> String:

kotlin
val hi: () -> String = ::hello

No inclous els parèntesis després de hello perquè vols desar la funció en una variable, no pas cridar-la.

Task

Copia el valor de la variable hi a una nova variable hey.

Crida la “funció” hey.

Show solution
kotlin
fun main() {

    fun hello(): String {
        return "Hello"
    }

    val hi = ::hello

    val hey = hi
    require(hey() == "Hello")
}

Referenciar no és executar

A continuació tens la funció ego que entra en bucle infinit:

kotlin
fun main() {

    fun ego() {
        while (true) {
            println("Ego is egocentric, only I 👺!")
        }
    }
}

Si executes aquesta funció, es comporta d’una manera bastant egocèntrica, es queda el programa només per ella:

Ego is egocentric, only I 👺 !
Ego is egocentric, only I 👺 !
Ego is egocentric, only I 👺 !
...

Si vols pots referenciar la funció ego: referenciar no és executar!

kotlin
fun ego() {
    while (true) {
        println("Ego is egocentric, only I 👺")
    }
}

val self = ::ego

println("No ego!")

Pots verificar que en aquest codi la funció ego no s’ha executat:

No ego!

High-order functions

Una funció pot acceptar una funció com a paràmetre.

Per exemple, la funció newList accepta com a segon paràmetre una funció de tipus (Int) -> Int:

kotlin
fun newList(list: List<Int>, f: (Int) -> Int): List<Int> {
    val newList = mutableListOf<Int>()
    for (item in list) {
        val newItem = f(item)
        newList.add(item)
    }
    return newList
}

La funció newList tornarà una llista nova en què a tots els seus elements seran …

kotlin
val list = newList(listOf(1, 2, 3), ::toto)

Qui sap, només ho sap toto! 🤔

Per exemple, toto podria ser aquesta funció:

kotlin
fun toto(a: Int): Int {
    return a + 5
}

val list = newList(listOf(1, 2, 3), ::toto)

require(list == listOf(6, 7, 8))

O aquesta altra:

kotlin
fun toto(a: Int): Int {
    return a * 5
}

val list = newList(listOf(1, 2, 3), ::toto)

require(list == listOf(5, 10, 15))

Funció anònima

Una funció anònima és aquella que no té nom.

S’utilitza quan la funció només es fa servir com a paràmetre d’una altra funció,

Per exemple, en lloc d’escriure:

kotlin
fun toto(a: Int): Int {
    return a + 5
}

val list = newList(listOf(1, 2, 3), ::toto)

require(list == listOf(6, 7, 8))

Pots assignar directament la funció a una variable:

kotlin
val toto = fun(a: Int): Int {
    return a + 5
}

I executar la funció referenciada per la variable toto com qualsevol altra funció:

kotlin
require(toto(10) == 15)

Ara la funció no té nom, només variables que referencien la funció:

kotlin
val tata = toto

require(tata(20) == 25)

val list = newList(listOf(1, 2, 3), tata)
require(list == listOf(6, 7, 8))

Per tant, pots passar directament una funció anònima com paràmetre d’una funció:

kotlin
val list = newList(
    listOf(1, 2, 3),
    fun(a: Int): Int {
        return a + 5
    }
)

require(list == listOf(6, 7, 8))

Funció genèrica

Les funcions poden tenir paràmetres genèrics, que s’especifiquen amb claudàtors angulars <> abans del nom de la funció.

La funció newList funciona molt bé amb Int, però no passa per a qualsevol altre tipus de dada.

La solució és fer-la genèrica per a qualsevol mena de dada parametritzant la funció newList amb T:

kotlin
fun <T> newList(list: List<T>, f: (T) -> T): List<T> {
    val newList = mutableListOf<T>()
    for (item in list) {
        val newItem = f(item)
        newList.add(item)
    }
    return newList
}

Ara ja la pots utilitzar amb String, per exemple:

kotlin
val list = newList(
    listOf("Joan", "Laura", "Roser"),
    fun(name: String): String {
        return name.uppercase()
    }
)

require(list == listOf("JOAN", "LAURA", "ROSER"))

O un data class definit per tu:

kotlin
data class Person(val name: String, val age: Int)

val list = newList(
    listOf(Person("Joan", 25), Person("Laura", 30)),
    fun(person: Person): Person {
        return person.copy(age = person.age + 10)
    }
)

require(list == listOf(Person("Joan", 35), Person("Laura", 40)))

Una altra millor és que la funció newList pugui acceptar una funció f que rep com a parametre un tipus de dada i pugui retornar un tipus de dada diferent.

Per tant, l’has de parametritzar amb dos paràmetres T i R:

kotlin
fun <T, R> newList(list: List<T>, f: (T) -> R): List<R> {
    val newList = mutableListOf<R>()
    for (item in list) {
        val newItem = f(item)
        newList.add(item)
    }
    return newList
}

Ara la funció toto té llibertat per tornar un tipus de dada diferent:

kotlin
val list = newList(
    listOf(Person("Joan", 25), Person("Laura", 30), Person("Mireia", 35)),
    fun(person: Person): String {
        return person.name
    }
)

require(list == listOf("Joan", "Laura", "Mireia"))

Però encara queda l’última millora que pots fer … Que accepti una funció que pot tornar valors nuls:

🥳 👻 😺

kotlin
fun <T, R> newList(list: List<T>, f: (T) -> R?): List<R> {

    val newList = mutableListOf<R>()
    for (item in list) {
        val newItem = f(item)
        if (newItem != null) {
            newList.add(newItem)
        }
    }
    return newList
}

La signatura de la funció és una mica T i R, però a banda d’això la implementació molt comprensible.

Ara si volem un llista amb el nom de totes les persones majors de 30 anys:

kotlin
val list = newList(
    listOf(Person("Joan", 25), Person("Laura", 30), Person("Mireia", 35)),
    fun(person: Person): String? {
        return if (person.age > 30) person.name else null
    }
)

require(list == listOf("Mireia"))

I pots verificar que pot tornar una llista buida si no hi ha persones majors de 50 anys:

kotlin
val list = newList(
    listOf(Person("Joan", 25), Person("Laura", 30), Person("Mireia", 35)),
    fun(person: Person): String? {
            return if (person.age > 50) person.name else null
    })

    require(list.isEmpty())

Expressió lambda

La funció toto és una funció d’un sol ús, contradient el fet que les funcions haurien de ser codi reutilitzable (que es fa servir moltes vegades).

Tampoc és un bloc de codi molt llarg que necessiti separar-se per fer el codi llegible.

Aquest tipus de funcions són molt habituals i es poden escriure de manera més concisa amb les expressions lambda.

kotlin
fun main() {

    val hi = { name: String -> "Hello $name"}

    require(hi("Laura") == "Hello Laura")
}
kotlin
val toto = { person: Person -> if (person.age > 30) person.name else null }

val list = newList(listOf(Person("Joan", 25), Person("Laura", 30), Person("Mireia", 35)), toto)

require(list == listOf("Mireia"))

I com tota variable, aquesta es pot passar en línia si només s’utilitza una sola vegada:

kotlin
val list = newList(
    listOf(Person("Joan", 25), Person("Laura", 30), Person("Mireia", 35)),
    { person: Person -> if (person.age > 30) person.name else null }
)

require(list == listOf("Mireia"))

Sintaxis

La forma sintàctica completa de les expressions lambda és la següent:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
  • Una expressió lambda sempre va envoltada per claus.

  • Les declaracions de paràmetres en la forma sintàctica completa van dins de les claus i poden tenir anotacions de tipus opcionals.

  • El cos va després de ->.

  • Si el tipus de retorn inferit de la lambda no és Unit, l’última (o possiblement única) expressió dins del cos de la lambda es tracta com el valor de retorn.

  • Si deixes fora totes les anotacions opcionals, el que queda té aquest aspecte:

kotlin
val sum = { x: Int, y: Int -> x + y }

Activitat

A continuació tens una funció genérica que recòrre tots els elements de la llista i la redueix a un únic valor.

Ella no sap com ha de procedir per fer la reducció, això ho sap la funció f.

kotlin
fun <T> reduce(list: List<T>, f: (T?,T) -> T): T? {
    var result: T? = null
    for (item in list) {
        result = f(result, item)
    }
    return result
}

Per exemple, si tinc aquesta llista:

kotlin
val numbers = listOf(7, 12, 3, 25, 18, 4, 9, 21, 14, 6, 30, 2, 11, 27, 5, 16, 8, 19, 23, 10)

Puc sumar tots els seus elements amb una funció anònima:

kotlin
val sum = reduce(numbers, fun(a: Int?, b: Int): Int {
    return if (a == null) b else a + b
})
require(sum ==  270)

O amb una funció lambda de manera més concisa:

kotlin
val sum = reduce(numbers, { a: Int?, b: Int -> if (a == null) b else a + b })
require(sum == 270)

Si et fixes, la funció reduce és genial! 😸

Funciona amb una llista d’un sol número:

kotlin
val numbers = listOf(7)
val sum = reduce(numbers, { a: Int?, b: Int -> if (a == null) b else a + b })
require(sum == 7)

Inclús amb una llista buida:

kotlin
val numbers = listOf<Int>()
val sum = reduce(numbers, { a: Int?, b: Int -> if (a == null) b else a + b })
require(sum == null)

😼

Passant lambdes finals

Si l’últim paràmetre d’una funció és una funció, llavors una expressió lambda passada com a argument corresponent es pot col·locar fora dels parèntesis:

kotlin
val numbers = listOf(7, 12, 3, 25, 18, 4, 9, 21, 14, 6, 30, 2, 11, 27, 5, 16, 8, 19, 23, 10)

val max = reduce(numbers) { a: Int?, b: Int ->
    if (a == null) b else if (a > b) a else b
}

require(max == 30)

Aquesta sintaxi també es coneix com a trailing lambdal.

Task

Calcula el valor mínim de la llista:

kotlin
val numbers = listOf(7, 12, 3, 25, 18, 4, 9, 21, 14, 6, 30, 2, 11, 27, 5, 16, 8, 19, 23, 10)
Show solution

Si la lambda és l’únic argument en aquesta crida, els parèntesis es poden ometre completament:

kotlin
fun run(f: () -> Unit) {
    f()
}


fun main() {

    run(fun() {
        println("Hello world!")
    })

    run {
        ->
        println("Hello")
    }


    run {
        println("Hello")
    }
}

L’última sintaxi és la més concisa, però si no saps d’on ve no pots entendre que és una funció lambda que s’està passant com argument a la funció run().

it: nom implícit d’un únic paràmetre

És molt habitual que una expressió lambda tingui només un paràmetre.

Al principi has creat la funció newList:

kotlin
fun <T, R> newList(list: List<T>, f: (T) -> R): List<R> {
    val newList = mutableListOf<R>()
    for (item in list) {
        val newItem = f(item)
        newList.add(item)
    }
    return newList
}

Si el compilador pot analitzar la signatura sense cap paràmetre, no cal declarar el paràmetre i es pot ometre ->.

El paràmetre es declararà implícitament amb el nom it:

kotlin
val numbers = listOf(10, 20, 30, 40)

val result = newList(numbers) { it * 2 }

require(result == listOf(20, 40, 60, 80))

Utilitza una funció com a tipus de retorn

Una funció és un tipus de dada, així que la pots fer servir com qualsevol altre tipus de dada.

Fins i tot pots retornar funcions des d’altres funcions.

kotlin
fun welcome(maybe: Boolean): (String) -> String {
    return if (maybe) {
        { name: String -> "Hello, $name! 🤗" }
    } else {
        { name: String -> "See you later, $name! 😤" }
    }
}

La funció welcome() retorna una funció diferent si maybe és true o false.

La funció que retorna welcome() es pot utilitzar com qualsevol altra funció:

kotlin
fun main() {

    require(welcome(true)("Roser") == "Hello, Roser! 🤗")
    require(welcome(false)("Patufet") == "See you later, Patufet! 😤")
}

Activitats

Task

A continuació tens un Map que guarda un conjunt functions que apliquen un descompte en funció del tipus de client:

kotlin
enum class CustomerTier { STANDARD, SILVER, GOLD, VIP }

fun main() {
    val discount: Map<CustomerTier, (Int) -> Int> = mapOf(
        CustomerTier.STANDARD to { 0 }, // no discount
        CustomerTier.SILVER to { (it * 0.05).toInt() }, // 5%
        CustomerTier.GOLD to { (it * 0.10).toInt() }, // 10%
        CustomerTier.VIP to {
            // 15% discount with a cap of $50
            val percent = (it * 0.15).toInt()
            val cap = 5000
            minOf(percent, cap)
        })
}

Calcula el descompte d’un client VIP amb un import de $1000:

Show solution
kotlin
assert(discount[CustomerTier.SILVER]?.invoke(100) == 5)
Task

Tens una llista de Int amb possibles valors nuls:

kotlin
val list: List<Int?> = listOf(5, null, 10, 8, null)

Amb la funció reduce que has escrit abans, suma els elements de la llista.

Show solution
kotlin
val list: List<Int?> = listOf(5, null, 10, 8, null)
val sum = reduce(list) { a, b -> if (a == null) b else if (b == null) a else a + b }
require(sum == 23)