Programación Funcional Básica en Scala

Agenda

  • Scala básico
  • Loops funcionales
  • Funciones de Orden Superior
  • Pattern matching
  • Estructuras de datos funcionales

Código fuente de los ejercicios en

https://github.com/bogota-lambda/basic-fp-exercises

Scala básico

// A comment!
/* Another comment */
/** A documentation comment */
object MyModule {

    def abs(n: Int): Int =
        if (n < 0) -n
        else n
    
    def formatAbs(x: Int): String = {
        val msg = "The absolute value of %d is %d"
        msg.format(x, abs(x))
    }
    
    def main(args: Array[String]): Unit =
        println(formatAbs(-42))

}
def abs(n: Int): Int = {
    val resultado =  if (n < 0) -n
                     else n
    resultado
}

Orientado a expresiones

def abs(n: Int): Int = {
    if (n < 0) -n
    else n
}
def abs(n: Int): Int =
    if (n < 0) -n
    else n
object MyApp {
    
    def main(args: Array[String]): Unit =
        println("Hello world!")

}
object MyApp extends App {

    println("Hello world!")

}

Loops Funcionales

Loops Funcionales

¿Cómo iterar en un lenguaje funcional?

Usando recursión

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

Caso base

Paso de reducción

¿Por qué es útil la recursión?

  • Muchas funciones se pueden definir naturalmente en términos de sí mismas
  • Las propiedades de funciones recursivas pueden ser demostradas usando inducción matemática

Loops funcionales

Problema: se llena la pila de ejecución

fact(4)    == 4 * fact(3)
           == 4 * ( 3 * fact(2) )
           == 4 * ( 3 * ( 2 * fact(1) ) )
           == 4 * ( 3 * ( 2 * 1 ) )
           == 4 * ( 3 * 2 )
           == 4 * 6
           == 24

Recursión de cola

Idea: si lo último que hace una función es llamarse a sí misma entonces el stack frame puede reusarse. Esto se denomina tail recursion o recursión de cola

La recursión de cola es un proceso iterativo

La recursión de cola permite escribir código tan eficiente en uso de la pila de ejecución como un ciclo while.

Recursión de cola

Reusando la pila de ejecución:

def fact(n: Int, acc: Int): Int =
    if(n <= 1)
      acc
    else
      fact(n-1, n*acc)

Loops funcionales

fact(4, 1) == fact( 3 , 4 * 1  )
           == fact( 3 , 4      ) 
           == fact( 2 , 3 * 4  ) 
           == fact( 2 , 12     ) 
           == fact( 1 , 2 * 12 ) 
           == fact( 1 , 24     ) 
           == 24 
def factorial(n: Int) : Int = {
    def go(n: Int, acc: Int): Int =
        if(n <= 1)
            acc
        else
            go(n-1, n*acc)
    
    go(n,1)
}

Encapsulando la implementación:

Ejercicio

¿Cómo computar la suma de unos números en un rango?

scala> sumInRange(1,2)
res0: Int = 3

scala> sumInRange(1,10)
res1: Int = 55

scala> sumInRange(6,6)
res2: Int = 6

scala> sumInRange(4,2)
res3: Int = 0

Solución

def sumInRangeNTR(start: Int, end: Int): Int =
    if(start > end)
      0
    else
      start + sumInRangeNTR(start+1, end)
def sumInRange(start: Int, end: Int): Int = {
    def go(n: Int, acc: Int): Int =
      if(n>end)
        acc
      else
        go(n+1, n+acc)
    go(start, 0)
}

Funciones de Órden Superior

Una función de orden superior lo puede serlo por dos razones:

1. Toma una función como argumento

2. Retorna una función como resultado

¿Por qué son útiles?

1. Expresiones idiomáticas comúnes pueden ser codificadas como funciones dentro del lenguaje en sí mismo: no hay necesidad de extender el lenguaje

2. Lenguajes de dominio específico pueden ser definidos como colecciones de funciones de orden superior

Formateando resultados

def abs(x: Int): Int =
    if(x<0)
      -x
    else
      x

def formatAbs(x: Int): String = {
    val msg = "The absolute value of %d is %d"
    msg.format(x, abs(x))
}

Formateando resultados

def formatFact(x: Int): String = {
    val msg = "The factorial of %d is %d"
    msg.format(x, fact(x))
}

¿Cómo podemos generalizar esto?

Formateando resultados

def formatFact(x: Int): String = {
    val msg = "The factorial of %d is %d"
    msg.format(x, fact(x))
}

¿Qué varía? ¿Qué se mantiene?

def formatAbs(x: Int): String = {
    val msg = "The absolute value of %d is %d"
    msg.format(x, abs(x))
}

Una función de orden superior

def formatResult(name: String, x: Int, f: Int => Int): String = {
    val msg = "The %s of %d is %d"
    msg.format(name, x, f(x))
}
def formatAbs2(x: Int): String = 
  formatResult("absolute value", x, abs)
def formatFact2(x: Int): String = 
  formatResult("factorial", x, fact)

Otro ejemplo: Encontrando elementos

def findFirst(strings: Array[String], key: String): Int = {
    def loop(n: Int): Int =
      if(n >= strings.length)
        -1
      else if(strings(n) == key)
        n
      else
        loop(n+1)
    loop(0)
}

Encontrando elementos

def findFirst(strings: Array[String], predicate: String => Boolean): Int = {
    def loop(n: Int): Int =
      if(n >= strings.length)
        -1
      else if(predicate( strings(n) ))
        n
      else
        loop(n+1)
    loop(0)
}

Generalizando un poco:

¿Esta función solamente debería funcionar para arreglos de Strings? ¿Hay algo en la implementación que solo funcione con Strings?

¿Qué podemos generalizar?

Parámetros de tipos

def findFirst[A](array: Array[A], predicate: A => Boolean): Int = {
    def loop(n: Int): Int = 
      if(n >= array.length - 1)
        -1
      else if(predicate( array(n) ))
        n
      else
        loop(n+1)
    loop(0)
}

Los tipos pueden ser un parámetro más:

Ejercicio

def isSorted[A](array: Array[A], ordered: (A,A) => Boolean ): Boolean

Escribir una función que indique si un arreglo está ordenado o no:

Algunos detalles:

Propiedad length de un array:

Para acceder al elemento i de un array:

Array(4,5,6).length == 3
val arr = Array(4,5,6)
arr(0) == 4
arr(2) == 6

Solución

def isSorted[A](array: Array[A], ordered: (A,A) => Boolean ): Boolean = { 
    def loop(n: Int): Boolean =
      if(n >= array.length-1)
        true
      else if(!ordered(array(n), array(n+1)))
        false
      else
        loop(n+1)
    loop(0)
}

Pattern Matching

Case Classes

Son clases con propiedades inmutables entre otras cosas

Permiten hacer pattern matching

case class Persona(nombre: String, edad: Int)

val laura = Persona("Laura", 33)
def saludar(persona: Persona): String = persona match {
    case Persona(nombre,edad) => s"Hola, $nombre"
}

Patrón contra el que se compara el valor

Resultado si el valor coincide con el patrón

Case Classes

También se pueden incluir predicados:

def mensajeVerificacionEdad(persona: Persona): String = 
    persona match {
        case Persona(nombre, edad) if edad >= 18 => 
            s"$nombre tiene mas de 18 años"
        case Persona(nombre, edad) =>
            s"$nombre es menor de edad"
    }

Expresando variantes

trait Usuario
case class Persona(id: String, nombre: String)  extends Usuario
case class Empresa(nit: String, nombre: String) extends Usuario
def toString(usuario: Usuario): String = usuario match {
    case Persona(nombre,id)  => 
        s"Persona con nombre $nombre e id $id"
    case Empresa(nombre,nit) => 
        s"Empresa con nombre $nombre y nit $nit"
}

Estructuras de datos funcionales

sealed trait List[+A]
case object Nil                             extends List[Nothing]
case class Cons[+A](head: A, tail: List[A]) extends List[A]

Listas Enlazadas

(Sobre varianza)

El + en la declaración List[+A] indica que "A es un tipo covariante o positivo de List"

Si por ejemplo el tipo Perro es subtipo de Animal entonces el tipo List[Perro] también será subtipo del tipo List[Animal]

Es decir gracias al + lo siguiente compila:

val perros  : List[Perro]  = List(perro1, perro2)
val animales: List[Animal] = perros

(Sobre varianza)

Nothing es un subtipo de todos los tipos

Como List es covariante eso quiere decir que para cualquier tipo A, tenemos que Nil se puede considerar un List[A]

Nil sirve para describir la lista vacía de cualquier tipo

sealed trait List[+A]
case object Nil                             extends List[Nothing]
case class Cons[+A](head: A, tail: List[A]) extends List[A]
Cons(1, Cons(2, Cons(3, Nil) ) )

Contruyendo Listas

Una forma de construir la lista 1 -> 2 -> 3 es:

Podemos crear una función constructora que haga esto más fácil:

object List {

    def apply[A](as: A*): List[A] = 
        if(as.isEmpty) {
            Nil
        } else {
            Cons( as.head, apply(as.tail: _*) )
        }

} 

Multiples argumentos de tipo A

Primer argumento

Otros argumentos

(como algo de tipo Seq[A])

Convierte un Seq[A] en una lista de argumentos varíadicos

List.apply( 1, 2, 3)

Contruyendo Listas

En scala cuando un método se llama apply se puede invocar normalmente:

Pero también se puede invocar así:

List( 1, 2, 3)

Con esto hemos logrado construir listas encadenadas de una forma más sencila:

List( 1, 2, 3) == Cons(1, Cons(2, Cons(3, Nil) ) )
List() == Nil

Funciones de Orden Superior en listas

scala> val myList = List(1,2,3)
myList: List[Int] = List(1, 2, 3)

scala> myList.map(x => 10*x)
res1: List[Int] = List(10, 20, 30)

map

Funciones de Orden Superior en listas

filter

scala> val myList = List(2,30,22,5,60,1)
myList: List[Int] = List(2, 30, 22, 5, 60, 1)

scala> myList.filter(x => x>10)
res2: List[Int] = List(30, 22, 60)

Funciones de Orden Superior en listas

find

scala> myList.filter(x => x>10)
res2: List[Int] = List(30, 22, 60)

scala> myList.find(x => x>10)
res3: Option[Int] = Some(30)

Funciones de Orden Superior en listas

flatMap

scala> val list = List(1,2,3,4,5)
list: List[Int] = List(1, 2, 3, 4, 5)

scala> def g(v:Int) = List(v-1, v, v+1)
g: (v: Int)List[Int]

scala> list.map(x => g(x))
res0: List[List[Int]] = List(List(0, 1, 2), List(1, 2, 3),
                             List(2, 3, 4), List(3, 4, 5), List(4, 5, 6))

scala> list.flatMap(x => g(x))
res1: List[Int] = List(0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6)

¿Cómo funciona map?

List(3 , 2 , 4)
List("abc", "xy" , "pqrs").map({ x: String => x.length })
Cons("abc", Cons("xy" , Cons("pqrs", Nil) ) )
Cons(  3  , Cons( 2   , Cons(  4   , Nil) ) )

Desestructurando:

¡Podríamos implementar map usando pattern matching!

Ejercicio

Implementar filter

¿Cuál es el caso base?

¿Cuándo una lista es no vacía que debemos hacer?

Solución

trait List[+A] {

    def filter(f: A => Boolean): List[A] = this match {
      case Nil        => 
        Nil
      case Cons(x,xs) => 
        if(f(x)) Cons(x, xs.filter(f)) else xs.filter(f)
    }

}

Solución

trait List[+A] {

    def filter(f: A => Boolean): List[A] = this match {
      case Nil                => 
        Nil
      case Cons(x,xs) if f(x) => 
        Cons(x, xs.filter(f))
      case Cons(_,xs)         =>
        xs.filter(f)
    }

}

Folds

¿Cómo combinar todos los elementos de una lista en uno solo?

def sum(list: List[Int]): Int = list match {
  case Nil        => 0
  case Cons(x,xs) => x + sum(xs)
}

def product(list: List[Int]): Int = list match {
  case Nil        => 1
  case Cons(x,xs) => x * product(xs)
}

def all(list: List[Boolean]): Boolean = list match {
  case Nil        => true
  case Cons(x,xs) => x && all(xs)
}

Folds

El tipo del resultado puede ser distinto al tipo del contenido de la lista:

def totalLength(list: List[String]): Int = list match {
  case Nil        => 0
  case Cons(x,xs) => x.length + totalLength(xs)
}

¿Qué varía? ¿Qué se mantiene?

foldRight

trait List[+A] {

    def foldRight[B](z: B, f (A,B) => B): B =
        this match {
            case Nil        => z
            case Cons(x,xs) => f(x, xs.foldRight(z, f))
        }

}

foldRight

List(a,b,c,...,x,y).foldRight(z,f)
==
f(a, f(b, f(c, ... f(x,f(y,z)) ... )

Ejercicio

Implementar sum, product, all y totalLength en términos de foldRight

Solución

def sum2(list: List[Int]): Int =
    list.foldRight[Int](0, (a,b) => a + b )

def product2(list: List[Int]): Int =
    list.foldRight[Int](1, (a,b) => a * b )

def all2(list: List[Boolean]): Boolean =
    list.foldRight[Boolean](true, (a,b) => a && b )

def totalLength2(list: List[String]): Int =
    list.foldRight[Int](0, (str,acc) => str.length + acc)

Programación Funcional Básica en Scala

By Miguel Vilá

Programación Funcional Básica en Scala

  • 1,667