Kotlin - Function type

Introducció

En Kotlin, les funcions es consideren construccions de primera classe.

Això vol dir que les funcions es poden tractar com un tipus de dades. Pots emmagatzemar funcions en variables, passar-les a altres funcions com a arguments i retornar-les des d’altres funcions.

Com passa amb altres tipus de dades que pots expressar amb valors literals —com un tipus Int amb valor 10 i un tipus String amb valor “Hello”— també pots declarar literals de funció, anomenats expressions lambda o, simplement, lambdes.

Desa una funció en una variable

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

Una funció declarada amb la paraula clau fun es pot cridar, cosa que fa que s’executi el codi del cos de la funció.

Com a construcció de primera classe, les funcions també són tipus de dades, de manera que pots desar funcions en variables, passar-les a funcions i retornar-les de funcions. Potser vols poder canviar el comportament d’una part de la teva app en temps d’execució o niar funcions composables. Tot això és possible gràcies a les expressions lambda.

fun main() {

    val niceFun = ::nice
    require(niceFun("Roser") == "🦋 ROSER")

}

fun nice(name: String): String {
    return "🦋 ${name.uppercase()}"
}

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

Quan executes el codi, es produeix un error perquè el compilador de Kotlin reconeix nice com el nom de la funció nice(), però espera que cridis la funció, en lloc d’assignar-la a una variable.

Function invocation 'nice()' expected.

Per referir-te a una funció com a valor, has d’utilitzar l’operador de referència de funció (::)

fun main() {

    val niceFun = ::nice
    require(niceFun("Roser") == "🦋 ROSER")

}

fun nice(name: String): String {
    return "🦋 ${name.uppercase()}"
}

Executa el codi per verificar que ja no hi ha més errors.

Redefineix la funció amb una expressió lambda

Les expressions lambda proporcionen una sintaxi concisa per definir una funció sense la paraula clau fun. Pots desar directament una expressió lambda en una variable sense una referència de funció a una altra funció.

Abans de l’operador d’assignació (=), afegeixes la paraula clau val o var seguida del nom de la variable, que és el que utilitzaràs quan cridis la funció.

Després de l’operador d’assignació (=) hi ha l’expressió lambda, que consisteix en un parell de claus {} i el contingut de la funció:

fun main() {

    val nice = { name: String -> "🦋 ${name.uppercase()}" }

    require(nice("Roser") == "🦋 ROSER")

}

Quan defineixes una funció amb una expressió lambda, tens una variable que fa referència a la funció.

També en pots assignar el valor a altres variables com qualsevol altre tipus i cridar la funció amb el nom de la nova variable.

fun main() {

    val nice = { name: String -> "🦋 ${name.uppercase()}" }

    val alsoNice = nice
    require(alsoNice("Roser") == "🦋 ROSER")
}

Amb les expressions lambda, pots crear variables que emmagatzemen funcions, cridar aquestes variables com si fossin funcions i desar-les en altres variables que també pots cridar com a funcions.

Utilitza les funcions com a tipus de dada

Has après que Kotlin té inferència de tipus. Quan declares una variable, sovint no cal especificar el tipus explícitament.

En l’exemple anterior, el compilador de Kotlin va poder inferir que el valor de nice era una funció.

Tanmateix, si vols especificar el tipus d’un paràmetre de funció o un tipus de retorn, has de conèixer la sintaxi per expressar tipus de funció.

Els tipus de funció consisteixen en un conjunt de parèntesis que contenen una llista opcional de paràmetres, el símbol -> i un tipus de retorn.

val nice = { name: String -> "🦋 ${name.uppercase()}" }

El tipus de dada de la variable nice que has declarat abans seria (String) -> String.

val nice: (String) -> String = { name: String -> "🦋 ${name.uppercase()}" }

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 main() {

    val hello = { name: String -> "Hello, $name! 🤗" }
    val bye = { name: String -> "See you later, $name! 😤" }

    fun welcome(maybe: Boolean): (String) -> String {
        return if (maybe) {
            hello
        } else {
            bye
        }
    }
}

La funció welcome() retorna la funció hello si maybe és true i retorna la funció bye si maybe és false.

Crea una variable anomenada welcomeTrue i assigna-li el resultat de cridar welcome(), passant true al paràmetre maybe.

Després, crea una segona variable, anomenada welcomeFalse, i assigna-li el resultat de cridar welcome(), aquest cop passant false al paràmetre maybe.

fun main() {

    // ...

    val welcomeTrue = welcome(true)
    require(welcomeTrue("Roser") == "Hello, Roser! 🤗")

    val welcomeFalse = welcome(false)
    require(welcomeFalse("Patufet") == "See you later, Patufet! 😤")
}

Tot i que no has cridat directament les funcions hello() o bye(), les has pogut cridar perquè vas desar els valors de retorn de cada vegada que vas cridar la funció welcome(), i vas cridar les funcions amb les variables welcomeTrue i welcomeFalse.

Ara ja saps com les funcions poden retornar altres funcions.

Passa una funció a una altra funció com a argument

També pots passar una funció com a argument a una altra funció.

Una funció que rep una altra funció com a argument et permet passar-hi una funció diferent cada vegada que es crida.

Activitat

Enunciat Implementa les parts marcades amb TODO a la plantilla següent:

  1. Declara lengthFn: (String) -> Int que retorni la longitud d’una cadena.
  2. Declara toIntRef fent servir referència de funció a String::toInt.
  3. Implementa applyTwice que rep (Int) -> Int i aplica l’operació dues vegades.
  4. Implementa makePrefixer que, donat un prefix, retorni (String) -> String.
  5. Declara maybeAction com a (() -> Unit)? i fes una invocació segura.
  6. Declara typealias Transformer i implementa combine que aplica dos transformers seqüencialment.
  7. Implementa compose que compos(i) dues funcions f i g: g(f(x)).

Plantilla

fun main() {
    // 1) Variable de tipus funció
    // TODO: lengthFn: (String) -> Int que retorni s.length
    val lengthFn: (String) -> Int = TODO()

    require(lengthFn("Kotlin") == 6)

    // 2) Referència de funció
    // TODO: toIntRef: (String) -> Int fent servir String::toInt
    val toIntRef: (String) -> Int = TODO()

    require(toIntRef("42") == 42)

    // 3) Passar funcions (higher-order) i aplicar dues vegades
    // TODO: applyTwice(op)(x) = op(op(x))
    fun applyTwice(op: (Int) -> Int): (Int) -> Int = TODO()

    val inc: (Int) -> Int = { it + 1 }
    require(applyTwice(inc)(10) == 12)

    // 4) Retornar funcions
    // TODO: makePrefixer(prefix) retorna (String) -> String que afegeix el prefix
    fun makePrefixer(prefix: String): (String) -> String = TODO()

    val ghost = makePrefixer("👻 ")
    require(ghost("Roser") == "👻 Roser")

    // 5) Tipus de funció nullable
    // TODO: maybeAction: (() -> Unit)? i invocació segura
    var maybeAction: (() -> Unit)? = null
    // Invocació segura que no falli
    // TODO
    maybeAction = { println("Ara sí!") }
    // Invocació segura que imprimeixi "Ara sí!"
    // TODO

    // 6) typealias i combinació de transformacions
    // TODO: Crea un typealias Transformer = (String) -> String
    // i implementa combine(a, b) que primer aplica 'a' i després 'b'
    // p.ex. combine(uppercase, wrap)("hi") == "[HI]"
    // TODO: typealias
    // TODO: combine
    val uppercase: (String) -> String = { it.uppercase() }
    val wrap: (String) -> String = { "[$it]" }

    // val tx = combine(uppercase, wrap)
    // require(tx("hi") == "[HI]")

    // 7) compose genèric
    // TODO: compose(f, g)(x) = g(f(x))
    // Signatura suggerida:
    // fun <A, B, C> compose(f: (A) -> B, g: (B) -> C): (A) -> C = ...
    // Prova:
    // val parseThenDouble = compose(String::toInt) { it * 2 }
    // require(parseThenDouble("21") == 42)
}

Solució (una possible)

typealias Transformer = (String) -> String

fun <A, B, C> compose(f: (A) -> B, g: (B) -> C): (A) -> C = { a -> g(f(a)) }

fun combine(a: Transformer, b: Transformer): Transformer = { s -> b(a(s)) }

fun applyTwice(op: (Int) -> Int): (Int) -> Int = { x -> op(op(x)) }

fun makePrefixer(prefix: String): (String) -> String = { s -> "$prefix$s" }

fun main() {
    val lengthFn: (String) -> Int = { s -> s.length }
    require(lengthFn("Kotlin") == 6)

    val toIntRef: (String) -> Int = String::toInt
    require(toIntRef("42") == 42)

    val inc: (Int) -> Int = { it + 1 }
    require(applyTwice(inc)(10) == 12)

    val ghost = makePrefixer("👻 ")
    require(ghost("Roser") == "👻 Roser")

    var maybeAction: (() -> Unit)? = null
    maybeAction?.invoke() // no fa res
    maybeAction = { println("Ara sí!") }
    maybeAction?.invoke() // imprimeix

    val uppercase: Transformer = { it.uppercase() }
    val wrap: Transformer = { "[$it]" }
    val tx = combine(uppercase, wrap)
    require(tx("hi") == "[HI]")

    val parseThenDouble = compose(String::toInt) { it * 2 }
    require(parseThenDouble("21") == 42)
}

TODO