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

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

fun main() {
fun hello(): String {
return "Hello"
}
val hi = ::hello
require(hi() == "Hello")
}

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

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.

Activitat

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

Crida la “funció” hey.

Referenciar no és executar

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

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!

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:

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 …

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

Qui sap, només ho sap toto! 🤔

Per exemple, toto podria ser aquesta funció:

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:

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:

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:

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

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

require(toto(10) == 15)

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

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

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:

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:

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:

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:

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:

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:

🥳 👻 😺

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:

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:

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.

fun main() {
val hi = { name: String -> "Hello $name"}
require(hi("Laura") == "Hello Laura")
}
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:

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:

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.

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:

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:

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:

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:

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:

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:

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.

Activitat

Calcula el valor mínim de la llista:

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

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

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:

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:

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.

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

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

Activitats

Activitat

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

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:

Activitat

Tens una llista de Int amb possibles valors nuls:

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

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