
Las funciones que no son puras
Tiempo de lectura: 4 minutos
Cada día son más los desarrolladores que conocen el paradigma de la programación funcional. Este paradigma promete código eficiente y libre de errores porque las funciones puras son más fáciles de probar y paralelizar.
En la práctica, las aplicaciones funcionales propiamente dichas siguen siendo algo abstracto. Sin embargo, algunos Concepts del paradigma de la programación funcional se aplican cada vez con más frecuencia a los lenguajes no funcionales. Además, este enfoque "parcialmente funcional" ayuda a resolver mejor muchos "problemas" habituales.
Hoy examinaremos más detenidamente uno de estos conceptos funcionales: las funciones puras/impuras.
NOTA: Utilizaremos pseudocódigo basado en Kotlin para definir nuestras funciones, manteniendo los ejemplos básicos para que sean más sencillos de seguir.
Funciones puras
La función pura es una función que no tiene efectos secundarios - en otras palabras, esta función no debe recuperar ni modificar ningún valor que no sean los valores pasados como params. De esta forma, cada llamada a la función con los mismos argumentos siempre dará como resultado la misma salida (valor devuelto). Empecemos por definir una función pura:
fun max(a: Int, b: Int) {
if (a > b)
return a
else
return b
}
La función max toma dos argumentos, dos números, y devuelve el mayor de ellos. Observe que esta función no accede ni modifica ningún valor fuera del ámbito de la función, por lo que es una función función pura. Podemos estar seguros de que llamar a esta función con los argumentos 2 y 7 devolverá SIEMPRE 7.
Para entender mejor este concepto, veamos la otra cara de la moneda: las distintas formas de romper la pureza de las funciones.
Funciones impuras
Un función impura es una función que tiene efectos secundarios - está modificando o accediendo a valores desde fuera del ámbito de la función (fuera del cuerpo de la función).
Efectos secundarios bastante evidentes
El ejemplo más sencillo es una función que modifica una propiedad externa para almacenar un estado.
val loalScore = 0
val getScore(score: Int): Int {
loalScore = score
return loalScore
}En este caso, la impureza no se manifiesta con fuerza porque las siguientes llamadas al método devolverán el mismo valor:
getScore(12) // returns 12
getScore(6) // returns 6
getScore(3) // returns 3Esta función modifica un valor externo pero sigue devolviendo el mismo valor en cada invocación debido a la asignación de valores. Sin embargo, no siempre es así. Consideremos otra función impura:
val addScore = 0
val addScore(score: Int): Int {
loalScore + score
return loalScore
}Esta función tiene una "impureza más fuerte" porque modifica un valor externo y devuelve un resultado diferente en cada invocación - el estado se almacena en la variable, fuera del ámbito de la función:
addScore(12) // returns 12
addScore(6) // returns 18
addScore(3) // returns 21El inconveniente de mantener el estado es que a veces las pruebas son más complejas; sin embargo, no podemos evitarlo en muchas aplicaciones.
La siguiente función está accediendo a un valor desde fuera del ámbito de la función, y al hacerlo se convierte en impura:
fun getString(length: Int): String {
return Random().nextString(length)
}Esta vez, cada llamada a la función con el mismo argumento tendrá como resultado un valor diferente:
getString(2) // returns "ab"
getString(2) // returns "hh"
getString(2) // returns "zk"Aunque los efectos secundarios presentados anteriormente son bastante fáciles de detectar, a menudo pueden ser más sutiles:
Efectos secundarios no tan evidentes
Un escenario interesante de efectos secundarios es la modificación del objeto pasado como argumento de una función:
fun increaseHeight(person: Person) {
person.height++
}Si se llama a esta función varias veces con la misma Person dará lugar a resultados diferentes porque el valor fuera de la función (almacenado en la instancia Persona) se modifica.
La excepción lanzada por una función es un excelente ejemplo de los efectos secundarios más difíciles de detectar:
fun addDistance (a:Int, b:Int): Int {
if(a < 0) {
throw IllegalAccessException("a must be >= 0")
}
return a + b
}
Otra forma interesante de crear efectos secundarios es simplemente llamando a otra función con efectos secundarios:
fun firstFunction() {
addDistance(-5, 7)
}
fun addDistance (a:Int, b:Int): Int {
if(a < 0) {
throw IllegalAccessException("a must be >= 0")
}
return a + b
}
Otro efecto secundario no tan obvio es el registro. Echemos un vistazo a este ejemplo de la vida real de nuestro Video Chat Base de Base:
private PublisherKit.PublisherListener publisherListener = new PublisherKit.PublisherListener() {
@Override
public void onStreamCreated(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamCreated: Publisher Stream Created. Own stream " + stream.getStreamId());
}
@Override
public void onStreamDestroyed(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamDestroyed: Publisher Stream Destroyed. Own stream " + stream.getStreamId());
}
@Override
public void onError(PublisherKit publisherKit, OpentokError opentokError) {
Log.d(TAG, "PublisherKit onError: " + opentokError.getMessage());
}
};En el código anterior, el registro como efecto secundario no afecta a la lógica de la aplicación, pero nos ayuda a entender lo que está pasando en la aplicación. Más adelante, es fácil para el desarrollador utilizar los datos devueltos por las retrollamadas e introducir más efectos secundarios.
Determinar si la función es pura-impura
Hay dos indicios de que una función puede ser impura: no toma argumentos ni devuelve ningún valor. Veamos el primer caso:
list.getItem(): StringEn el ejemplo anterior, la función no toma ningún parámetro, pero devuelve el valor. Esto significa que, muy probablemente, el valor se recupera del estado de la clase. Veamos qué ocurre cuando una función no devuelve ningún valor:
list.setItem("item")Mirando el nombre de la función, podemos decir que el parámetro se utilizará probablemente para modificar el estado de la clase.
Y por último, podemos tener un combo en el que no hay argumento ni valor devuelto:
list.sort()
Son sólo indicios. No siempre es así, pero estas pistas suelen ser buenos indicadores de pureza.
Resumen
En el paradigma de la programación funcional, lo ideal es que todas las funciones sean puras.
Sin embargo, en muchas aplicaciones del mundo real, las cosas no son tan binarias. A veces no se pueden evitar las funciones impuras, especialmente si una aplicación requiere recursos externos como persistencia, entrada de usuario o acceso a datos de red. Tenerlos rompe la pureza de la función y de toda la aplicación, lo cual no está mal.
Normalmente tendremos una mezcla de funciones puras e impuras en una misma aplicación. Es una buena práctica ser consciente de la pureza/impureza, ya que facilita las pruebas de la aplicación y nos ayuda a evitar errores.