Cursos‎ > ‎Cursadas Anteriores‎ > ‎2010‎ > ‎Jueves Mañana‎ > ‎Funcional‎ > ‎

Paradigma Funcional - Clase 1

Introducción

  • Características del paradigma:
    declarativo, basado en funciones, no efecto de lado, si son funciones devuelven un único resultado, no hay órdenes: sólo expresiones.

  • Características del lenguaje:
    fuertemente tipado

  • Características del entorno de trabajo:
    al igual que en prolog tenemos programa por un lado y un entorno de consultas por otro.

Elementos básicos del lenguaje

No hay muchas cosas novedosas, así que rápidamente podemos enumerar qué herramientas aparecen en el nuevo lenguaje:
  • Números, que se pueden sumar, restar, comparar, sqrt
  • Booleanos, que podemos operar con: && || not
  • Listas, son muy parecidas a las de prolog aunque la sintaxis puede ser un poco diferente: [1,2,3], [1..3], [1,3..11]
    • por ahora podemos saber longitud, concatenar, sum, !!, elem, head, tail. 
    • Al no haber efecto de lado, obviamente las listas no se pueden modificar
  • Tuplas con fst y snd.
  • Para todos vale comparar con == y /=
Luego hablamos de funciones:
  • Algunos ejemplos de funciones que mencionamos antes son length y sum
  • Ojo, también + y ++.
    • La única diferencia es que se escriben en forma infija, en contraposición a las anteriores que utilizan forma prefija.
    • Cualquier operador es una función, y es posible definir nuestras propias funciones haciendo eso.
  • Analizamos la cantidad de parámetros de diferentes funciones, repasamos el concepto de aridad.
  • Definimos el concepto de aplicación y mostramos que a diferencia de lo que suele pasar, en Haskell no se utiliza paréntesis para aplicar

Elementos básicos del lenguaje

En la máquina podemos ver los tipos de algunas cosas, por ejemplo:
  • 1 tiene tipo Num (por ahora vamos a obviar el Int a => a, lo consideramos equivalente a Int.
  • True tiene tipo Bool
  • [1,2,3] tiene tipo [Num]
  • ("Juan", 23) tiene tipo ([Char], Int)
También de las funciones podemos conocer su tipo, por ejemplo:
  • not tiene tipo Bool -> Bool
  • + tiene tipo Num -> Num -> Num (por ahora podemos considerarlo equivalente a (Num x Num) -> Num, más adelante se verá la diferencia).
El hecho de que podamos preguntarnos por el tipo de una función, nos muestra que también las funciones son valores. es decir, se rompe la frontera entre la función y el valor.

Definición de funciones

  • Comenzamos definiendo la función doble, la forma más sencilla podría ser:
    doble x = 2 * x

  • Una vez hecho esto, podemos preguntar el tipo de doble, y obtenemos Num -> Num

  • Para reforzar aprovechamos a definir también una función de dos parámetros, a modo de ejemplo:
    esDivisiblePor x y = (y `mod` x) == 0

Ejercitación

  • Antes de continuar podemos hacer algunos ejercicios:
    primerElementoPar lista = even (head lista)

    sumarComponentest tupla = fst tupla + snd tupla

    longMayorQue string long = length string > long

Evaluación de funciones: sustituciones

  • Comenzamos con un ejemplo en el que podemos agregar esta definición de función
    f = 3 + doble x

  • Y nos detenemos a analizar cuál es el proceso para calcular el valor de f 4:
    f 4
    3 + doble 4
    3 + (2 * 4)
    3 + 8
    11

  • Podemos ver el concepto de sustitución al pasar del segundo paso al tercero, ¿qué pasa ahí?
    • Tomamos la parte doble 4
    • Vemos que tenemos una definición para resolver eso: doble x = 2 * x
    • Podemos entonces reemplazar doble 4 por 2 * x, poniendo 4 donde decía x, es decir: 2 * 4.

  • Hacemos notar que no hay que entender 3 + doble 4 como si fuera un texto, el reemplazo se hace en una estructura jerárquica, la jerarquía puede entenderse así:
    • Es decir, el nodo principal es la suma (+), que tiene dos hijos (cada uno de sus operandos).
    • A la izquierda un operando simple (3)
    • A la derecha nos quedaría (doble 4)
      • Que podemos representarlo como un nodo doble con un único hijo 4.

    En todos los casos, las funciones son los nodos internos del árbol, mientras que los valores simples son sus hojas. Cada función tiene como subárboles a cada uno de sus parámetros.

Pattern Matching

  • El pattern matching nos permite dos cosas:
    • Poner más de una definición de una función para que el matching determine cuál de las dos versiones se debe usar en cada caso.
    • Al recibir un valor compuesto usar el encabezado de la función para separar sus componentes, esto vale tanto para listas como para tuplas.

  • Esto es similar a lo que ya hicimos en lógico, con la salvedad de que en lógico, al trabajar con relaciones, todas las variantes de un predicado producen resultados, en cambio en funcional siempre se evaluará sólo una de las definiciones de la función.

  • Ejemplos básicos:
    esPar 0 = True
    esPar 1 = False
    esPar x = esPar (x - 2)

    distancia (x1,y1) (x2,y2) = sqrt ((x1-x2)**2 + (y1-y2)**2)

  • En una función que recibe tuplas, podemos recibirlas tanto con una variable como con un patrón más complejo, por ejemplo:
    sumarComponentes (a,b) = a + b

    es equivalente a:
    sumarComponentes t = fst t + snd t

    Es importante manejar ambas posibilidades y elegir cuál conviene más en cada caso.

  • Cuando usamos un patrón para desarmar un valor compuesto que recibimos por parámetro, podemos usar _ para designar las partes del valor compuesto que no nos interesan, por ejemplo si representáramos fechas con una terna (dia, mes, año), podriamos hacer la función:
    diaDelMes (dia,_,_) = dia

Guardas

  • Las guardas también nos permiten establecer múltiples definiciones de una función. Una guarda es una expresión booleana que define si esa definición se puede utilizar, por ejemplo:
    puntosLocal (golesLocal, golesVisitante)
    | golesLocal >  golesVisitante = 3
    | golesLocal == golesVisitante = 1
    | otherwise                    = 0

    La expresión otherwise nos permite definir una rama que se evaluará en caso que todas las anteriores fallen.

  • Al igual que en el caso anterior, es importante destacar que al estar hablando de funciones, sólo una de las ramas puede ser evaluada.

  • Muy importante:
    No debemos usar las guardas en reemplazo de las expresiones booleanas, por ejemplo la siguiente definición la consideraremos errónea:
    primeraComponenteIgual t1 t2
    | fst t1 == fst t2 = True
    | otherwise        = False

    La forma correcta es usar la expresión booleana:
    primeraComponenteIgual t1 t2 = fst t1 == fst t2

    O bien:
    primeraComponenteIgual (x1,_) (x2,_) = x1 == x2

    Normalmente el uso de guardas en una función que devuelve un booleano es causa de este tipo de error, y muestra una forma procedural de pensamiento.

Manipulación de funciones

La característica principal del paradigma funcional es la operación con funciones. Para eso vamos a definir trés formas básicas de operar con funciones:
  • Composición
  • Aplicación parcial
  • Funciones de orden superior
En esta clase nos concentraremos en las dos primeras.

Composición

  • La composición es similar a la que hemos visto en matemática y nos permite definir una función fog que entendemos como "la composición de f con g" y se define (en matemática) como (fog) (x) = f (g (x))

    En Haskell vamos a utilizar un "." en lugar del famoso "cerito" (o). Podemos usarlo para definir una función como la composición de otras dos, por ejemplo
        isUpper . head

    Nos permite construir una función que recibe un String y me dice si la primera letra es mayúscula.

  • Para poder aplicarle parámetros a esa función, es importante poner la composición entre paréntesis, ya que la aplicación de funciones tiene mayor precedencia que la composición (esto es importante, no te quedes sin entenderlo!):
        > (isUpper . head) "Hola"
        True

  • También podemos ponerle un nombre a la función compuesta, así:
        empiezaConMayúscula = isUpper . head

    Y luego la podríamos utilizar así:
        > empiezaConMayúscula "hola"
        False

    Nótese que esta forma de definir funciones es sutilmente distinta a la que estábamos acostumbrados. Con lo que sabíamos hasta ahora podríamos haber hecho:
        empiezaConMayúscula unString = isUpper (head unString)

    Cuando definimos funciones por composición, podemos definir directamente la función sin necesidad de hablar de los parámetros.

  • Podemos ver que la composición nos da una forma de definir una nueva función sin necesidad de ponerle un nombre ni ponerla entre nuestras definiciones de funciones.
    Es decir, sqrt . doble, es una función como cualquier otra y puedo hacer con ella lo mismo que hago con las demás funciones, por ejemplo:
    • Aplicarle parámetros (como en el ejemplo anterior).
    • Consultar su tipo.

    Para trabajar un poco más con los tipos de funciones compuestas podemos analizar el tipo de even . length

  • La composición no parece un asunto complicado, sin embargo es importante destacar algunos detalles para evitar errores muy comunes:
    • que sólo (f.g) x se considera composición. Si yo hago f(g x) no estoy utilizando composición.
    • la composición puede hacerse únicamente entre funciones, es decir, yo puedo componer sqrt con doble pero no sqrt con 4 (porque 4 es un número y no una función) y tampoco sqrt con doble 8 (doble 8 también es un número).
    • (por lo anterior) los paréntesis aquí si son necesarios.
      Si yo pongo, sin paréntesis, sqrt . doble 8, eso se entiende como composición entre sqrt y doble 8, debo utilizar los paréntesis para indicar que mi intención es componer sqrt con doble y luego a la función resultante aplicarle el parámetro 8.

Orden superior

  • El hecho de que las funciones sean valores significa que además de aplicarles parámetros o consultar su tipo podemos hacer cualquier cosa que hacemos con los demás valores, en particular usarlas como parámetro de otras funciones.

  • Esto es análogo a lo que ya vimos en lógico con respecto a los predicados de orden superior. La diferencia es que en lógico nunca definimos un predicado de orden superior, sólo los usamos, aquí vamos a animarnos a definir funciones de orden superior.
    (Ojo, no es que en lógico no se pueda, simplemente no lo hicimos por falta de tiempo.)

  • Un ejemplo sencillo de una función que recibe otra por parámetro puede ser:
    aplicaDosVeces f x = f (f x)
  • Podemos ver algunos ejemplos de orden superior sencillitos
    tup f g (x,y) = (f x, g y)
    aplicarATupla fun (x,y) = (fun x, fun y)
    tuplizar fun1 fun2 x = (fun1 x, fun2 x)
    comparar op (x,y) = x `op` y
    cuarta = f . f

Para después de la clase

Comments