Funciones y Programación Funcional

Definición y uso de funciones

Las funciones son bloques de código reutilizables que realizan una tarea específica. En Scala, las funciones son ciudadanos de primera clase, lo que significa que pueden ser definidas, pasadas como parámetros y devueltas como valores.

Definición de Funciones

Ejemplo Básico

Vamos a definir una función simple que suma dos números y devuelve el resultado.

				
					// Definición de la función
def suma(a: Int, b: Int): Int = {
  // Cuerpo de la función
  // Suma los dos parámetros y devuelve el resultado
  a + b
}

// Uso de la función
val resultado = suma(3, 5)
println(resultado) // Imprime: 8

				
			

Explicación

  • def: Se utiliza para definir una función.
  • suma: Es el nombre de la función.
  • (a: Int, b: Int): Son los parámetros de la función, a y b, ambos de tipo Int.
  • Int: Especifica que la función retorna un valor de tipo Int.
  • { a + b }: Es el cuerpo de la función, que suma los dos parámetros y devuelve el resultado.
  • val resultado = suma(3, 5): Llama a la función suma con los argumentos 3 y 5, y asigna el resultado a la variable resultado.
  • println(resultado): Imprime el resultado en la consola.

Funciones de Orden Superior

Las funciones de orden superior son funciones que toman otras funciones como parámetros o devuelven funciones.

Ejemplo de Función de Orden Superior

				
					// Definición de una función de orden superior
def aplicarOperacion(a: Int, b: Int, operacion: (Int, Int) => Int): Int = {
  // Llama a la función operacion con los parámetros a y b
  operacion(a, b)
}

// Definición de una función que se usará como parámetro
def multiplicacion(a: Int, b: Int): Int = {
  a * b
}

// Uso de la función de orden superior
val resultadoMultiplicacion = aplicarOperacion(4, 6, multiplicacion)
println(resultadoMultiplicacion) // Imprime: 24

				
			

Explicación

  • def aplicarOperacion(a: Int, b: Int, operacion: (Int, Int) => Int): Int: Define una función de orden superior que toma dos enteros y una función como parámetros y devuelve un entero.
  • operacion(a, b): Llama a la función operacion con los parámetros a y b.
  • def multiplicacion(a: Int, b: Int): Int: Define una función multiplicacion que toma dos enteros y devuelve su producto.
  • val resultadoMultiplicacion = aplicarOperacion(4, 6, multiplicacion): Llama a la función aplicarOperacion con los argumentos 4, 6, y la función multiplicacion, y asigna el resultado a la variable resultadoMultiplicacion.
  • println(resultadoMultiplicacion): Imprime el resultado en la consola.

Expresiones lambda

Las expresiones lambda (o funciones anónimas) son una característica poderosa en Scala que permite definir funciones sin un nombre explícito. Son útiles para pasar funciones como argumentos o para definir funciones rápidamente sin la necesidad de una definición completa.

Sintaxis Básica

La sintaxis básica de una expresión lambda es la siguiente:

				
					// Ejemplo de expresión lambda para suma
val suma = (a: Int, b: Int) => a + b
val resultadoSuma = suma(3, 5)
println(resultadoSuma) // Imprime: 8

// Ejemplo de expresión lambda con map
val numeros = List(1, 2, 3, 4, 5)
val cuadrados = numeros.map(n => n * n)
println(cuadrados) // Imprime: List(1, 4, 9, 16, 25)

// Ejemplo de expresión lambda con filter
val pares = numeros.filter(n => n % 2 == 0)
println(pares) // Imprime: List(2, 4)

// Ejemplo de expresión lambda con reduce
val sumaTotal = numeros.reduce((a, b) => a + b)
println(sumaTotal) // Imprime: 15

				
			

Explicación

  • Expresión Lambda para Suma:

    • val suma = (a: Int, b: Int) => a + b: Define una expresión lambda que toma dos parámetros a y b, ambos de tipo Int, y devuelve su suma.
    • resultadoSuma = suma(3, 5): Llama a la expresión lambda suma con los argumentos 3 y 5, y asigna el resultado (8) a la variable resultadoSuma.
    • println(resultadoSuma): Imprime 8 en la consola.
  • Expresión Lambda con map:

    • val cuadrados = numeros.map(n => n * n): Utiliza la función map en la lista numeros para aplicar una expresión lambda que eleva cada elemento n al cuadrado.
    • println(cuadrados): Imprime la lista resultante [1, 4, 9, 16, 25] en la consola.
  • Expresión Lambda con filter:

    • val pares = numeros.filter(n => n % 2 == 0): Utiliza la función filter en la lista numeros para aplicar una expresión lambda que selecciona solo los números pares (n % 2 == 0).
    • println(pares): Imprime la lista resultante [2, 4] en la consola.
  • Expresión Lambda con reduce:

    • val sumaTotal = numeros.reduce((a, b) => a + b): Utiliza la función reduce en la lista numeros para aplicar una expresión lambda que suma todos los elementos de la lista secuencialmente.
    • println(sumaTotal): Imprime 15, que es la suma total de los elementos de la lista [1, 2, 3, 4, 5], en la consola.

Closures

En Scala, los closures son funciones que capturan y operan con variables del ámbito que las rodea, incluso después de que ese ámbito ha sido cerrado o salido. Esto significa que una función lambda puede acceder y modificar variables que no están definidas dentro de la función lambda misma, sino en el entorno circundante.

Ejemplo de Closure

				
					def multiplicador(factor: Int): Int => Int = {
  (x: Int) => x * factor
}

val multiplicarPorDos = multiplicador(2)
val resultado1 = multiplicarPorDos(5) // 5 * 2 = 10

val multiplicarPorTres = multiplicador(3)
val resultado2 = multiplicarPorTres(5) // 5 * 3 = 15

println(resultado1) // Imprime: 10
println(resultado2) // Imprime: 15

				
			

Explicación

  • Definición de la Función multiplicador:

    • def multiplicador(factor: Int): Int => Int = { ... }: Define una función llamada multiplicador que toma un parámetro factor de tipo Int y devuelve una función que toma un parámetro x de tipo Int y devuelve x * factor.
  • Uso del Closure:

    • val multiplicarPorDos = multiplicador(2): Llama a la función multiplicador con el argumento 2, lo que devuelve una función que multiplica su argumento por 2. multiplicarPorDos se convierte en un closure que captura el valor 2 para factor.
    • val resultado1 = multiplicarPorDos(5): Llama a multiplicarPorDos con el argumento 5, lo que resulta en 5 * 2 = 10.
  • Nuevo Uso del Closure:

    • val multiplicarPorTres = multiplicador(3): Llama a la función multiplicador con el argumento 3, lo que devuelve una función que multiplica su argumento por 3. multiplicarPorTres se convierte en otro closure que captura el valor 3 para factor.
    • val resultado2 = multiplicarPorTres(5): Llama a multiplicarPorTres con el argumento 5, lo que resulta en 5 * 3 = 15.
  • Impresión de Resultados:

    • println(resultado1): Imprime 10, que es el resultado de 5 * 2.
    • println(resultado2): Imprime 15, que es el resultado de 5 * 3.

En este ejemplo, las funciones multiplicarPorDos y multiplicarPorTres son closures que capturan el valor del parámetro factor pasado a la función multiplicador. Aunque factor es una variable local en multiplicador, las funciones creadas (multiplicarPorDos y multiplicarPorTres) retienen una referencia a este valor incluso después de que multiplicador haya terminado de ejecutarse. Esto es lo que se conoce como el comportamiento de closure en Scala.

Recursión y tail-recursion

La recursión es una técnica en programación donde una función se llama a sí misma para resolver un problema de manera iterativa. En Scala, la tail-recursion (recursión de cola) es una forma especial de recursión donde la llamada recursiva es la última operación realizada por la función. Esta técnica es importante porque evita el riesgo de desbordamiento de la pila (stack overflow) cuando se trabaja con conjuntos grandes de datos.

Ejemplo de Recursión Simple

Vamos a ver un ejemplo simple de una función recursiva que calcula el factorial de un número:

				
					def factorial(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial(n - 1)
}

val resultado = factorial(5)
println(resultado) // Imprime: 120

				
			

Explicación

  • Función factorial:

    • def factorial(n: Int): Int = { ... }: Define una función factorial que toma un parámetro n de tipo Int y devuelve un Int.
    • if (n <= 1) 1: Caso base de la recursión. Si n es menor o igual a 1, devuelve 1.
    • else n * factorial(n - 1): Caso recursivo. Para números mayores que 1, calcula n * factorial(n - 1).
  • Uso de la Función factorial:

    • val resultado = factorial(5): Llama a la función factorial con el argumento 5.
    • println(resultado): Imprime 120, que es el resultado del factorial de 5.

Tail-Recursion en Scala

La tail-recursion es una optimización que permite a Scala convertir llamadas recursivas en iteraciones eficientes, utilizando la misma porción de la pila en cada llamada. Para que una función recursiva sea tail-recursive, la llamada recursiva debe ser la última operación realizada por la función.

Ejemplo de Tail-Recursion

Vamos a reescribir la función factorial utilizando tail-recursion:

Expresiones condicionales

Las expresiones condicionales en Java permiten evaluar condiciones y tomar decisiones basadas en esas evaluaciones. La expresión condicional más común es el operador ternario (? :), que proporciona una manera concisa de realizar una operación condicional simple.

Código del Programa

				
					import scala.annotation.tailrec

def factorialTR(n: Int): Int = {
  @tailrec
  def factorialHelper(acc: Int, num: Int): Int = {
    if (num <= 1) acc
    else factorialHelper(acc * num, num - 1)
  }

  factorialHelper(1, n)
}

val resultadoTR = factorialTR(5)
println(resultadoTR) // Imprime: 120

				
			

Explicación

  • Función factorialTR:

    • import scala.annotation.tailrec: Importa la anotación tailrec para indicar que se espera que la función factorialHelper sea tail-recursiva.
    • def factorialTR(n: Int): Int = { ... }: Define una función factorialTR que calcula el factorial de n.
    • def factorialHelper(acc: Int, num: Int): Int = { ... }: Define una función interna factorialHelper que realiza la recursión de cola.
    • @tailrec: Anota la función factorialHelper como tail-recursiva para que el compilador verifique que realmente es tail-recursiva.
    • factorialHelper(1, n): Llama a factorialHelper con 1 como acumulador inicial y n como el número para calcular su factorial.
  • Uso de la Función factorialTR:

    • val resultadoTR = factorialTR(5): Llama a la función factorialTR con el argumento 5.
    • println(resultadoTR): Imprime 120, que es el resultado del factorial de 5.

Conclusiones

  • La recursión es una técnica poderosa pero puede llevar al desbordamiento de la pila si no se maneja adecuadamente.
  • La tail-recursion en Scala es una forma eficiente de escribir funciones recursivas, ya que el compilador puede optimizarlas para evitar problemas de desbordamiento de pila.
  • Es importante utilizar la anotación @tailrec para asegurarse de que la función realmente sea tail-recursiva y que el compilador pueda aplicar la optimización correspondiente.