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:

java
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ó (::):

java
fun main() {

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

    val hi = ::hello

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

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

java
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
java
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:

java
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!

java
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:

java
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 …

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

Qui sap, només ho sap toto! 🤔

Per exemple, toto podria ser aquesta funció:

java
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:

java
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:

java
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:

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

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

java
require(toto(10) == 15)

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

java
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ó:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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:

🥳 👻 😺

java
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:

java
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:

java
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.

java
fun main() {

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

    require(hi("Laura") == "Hello Laura")
}
java
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:

java
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:

java
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.

java
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:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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.

java
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ó:

java
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:

java
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
java
assert(discount[CustomerTier.SILVER]?.invoke(100) == 5)
Task

Tens una llista de Int amb possibles valors nuls:

java
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
java
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)