Programación funcional - UTN

Anuncio
Paradigmas de Programación
Programación funcional
Introducción
La visión tradicional de programación sólo tiene en cuenta la secuencia de
instrucciones que deben suministrarse a una computadora para su operación. Una
visión más amplia de programación debe contemplar además otros aspectos.
Entonces, podría definirse programación como el análisis y solución de problemas
mediante la descripción de valores, propiedades y métodos, el diseño de
algoritmos correctos y eficientes y de estructuras de datos que faciliten las
soluciones, y la codificación de los algoritmos en un formato adecuado para su
ejecución por una computadora. De esta manera, la visión tradicional
(programación imperativa) queda comprendida como una parte pequeña de dicha
tarea. La programación funcional intenta tratar el problema de la programación
desde un punto de vista matemático, utilizando la noción de función como base
para la construcción de los algoritmos y estructuras de datos.
Los lenguajes de programación imperativos evolucionaron a partir de los
lenguajes ensambladores, cuyo único objetivo era el de proveer una forma de
controlar el comportamiento de una computadora. El proceso de pensamiento de
los programadores no estaba contemplado en el diseño de dichos lenguajes. De
esta manera. los lenguajes imperativos reflejan en gran medida la arquitectura de
las computadoras, y por lo tanto resulta complicado razonar sobre los programas
(por ejemplo, conocer sus propiedades). Los lenguajes de programación
imperativos de alto nivel abstraen muchas de las características particulares de la
computadora en la que corren, pero no alcanzan a abstraer la arquitectura sobre la
que operan.
En los lenguajes imperativos existen dos tipos de construcciones: los
comandos y las expresiones. Los comandos permiten manejar explícitamente el
flujo de control, y las expresiones son utilizadas para calcular valores.
Por otro lado, los lenguajes imperativos poseen la característica de poder
realizar modificaciones implícitas a la memoria de la computadora. A estas
modificaciones se las conocen como "efectos laterales", ya que no son claramente
visibles en el código del programa, y tienen consecuencias no deseables para el
razonamiento de propiedades. Por ejemplo, la proposición x = x, ¿es verdadera
para cualquier x En matemáticas uno espera que así sea, y así lo es. Pero en un
lenguaje imperativo, esta propiedad tan importante no se cumple; alcanza con
reemplazar x con alguna expresión que contenga efectos laterales, tal como read o
random. Esto sucede pues las expresiones y los comandos pueden entremezclarse
y agregar de esa manera efectos laterales a las primeras. Dichos efectos laterales,
que alteran radicalmente la capacidad de razonamiento al inducirnos a
malinterpretar lo que escribimos, son la característica fundamental de operación de
Programación Funcional - Página 1
Paradigmas de Programación
los lenguajes imperativos. Sería altamente deseable que la mismísima estructura
del lenguaje impidiese este tipo de "malentendidos".
¿Cuál es el modelo en el que se basan los lenguajes imperativos? Existen dos
componentes fundamentales: la unidad de procesamiento (CPU) y la memoria, y
ambas están conectadas de manera de poder intercambiar información.
cpu
Memoria
Cuello de botella
Esta conexión puede hacerse con pequeñas cantidades de información por
vez (p. ej. algunos bytes), Pero actualmente se utilizan técnicas como el caching o el
paralelismo, para aumentar la cantidad de información que pasa por este canal. La
CPU entiende instrucciones que le permiten modificar la memoria, y las mismas
deben tener un orden determinado (secuencia explícita de instrucciones) para poder
conocer con exactitud los cambios a realizar. La memoria determina la existencia
de un estado implícito, que puede ser alterado (mediante asignaciones a la misma),
dando por resultado un lenguaje dinámico. Al finalizar la ejecución de un
programa, el estado implícito contiene el valor de resultado (además de muchos
otros valores, irrelevantes al resultado del cálculo que se quería realizar). Como ya
se vio, la secuencia de instrucciones que modifica implícitamente el estado dificulta
el proceso de razonamiento. Como ejemplo, puede considerarse el siguiente código
que muestra un programa para calcular el factorial de un número.
Procedure fac(x)
n : = x;
a : = 1;
while(n > 0) do
a : = a * n;
n : = n − 1;
end while
return a
end proc
Una manera de aligerar estos problemas es tratar de abstraer algunas de
estas nociones (p. ej. mediante procedimientos), pero cuanto mayor es el nivel de
abstracción, mayor es el alejamiento del modelo subyacente. Sería deseable que todas
Programación Funcional - Página 2
Paradigmas de Programación
esta inconveniencias fueran evitadas, y la capacidad de razonar fuera restaurada.
Para ello se pueden estudiar las características que hacen que el razonamiento sea
sencillo en matemáticas.
En matemáticas no existe la noción de estado implícito que puede ser
modificado, haciendo innecesaria la presencia de instrucciones. En cambio existen
valores (inmutables) que pueden ser expresados de maneras complejas mediante
expresiones; el conjunto de valores conocidos conforman de esta manera un estado
explícito, dando como resultado un lenguaje estático. El cálculo de dichos valores se
realiza mediante un proceso de reemplazo de subexpresiones que no tienen un
orden preestablecido, dando como resultado un control implícito de operación. Al
no existir efectos laterales, dos expresiones sintácticamente iguales darán el mismo
valor, propiedad conocida como transparencia referencial; esta propiedad es el pilar
de la habilidad de razonamiento. Por ello, aumentar el nivel de abstracción
mantiene la coherencia con el modelo subyacente. Lo siguiente es la definición
matemática de la función factorial:
fac (n )
1
, si n=0
n*fac(n-1)
, si n>0
Al comparar esta definición con el programa imperativo anterior, se puede
ver claramente que el estado implícito dificulta la capacidad de entender y conocer
las propiedades de este cómputo.
Dado que las expresiones son construcciones conocidas en los lenguajes de
programación, y que las matemáticas las utilizan como base para la denotación de
valores y propiedades, ¿por qué no considerar un lenguaje de programación
compuesto únicamente por expresiones? Un programa en tal lenguaje será un
conjunto de expresiones que denotan valores (con ciertas propiedades). Para que
este lenguaje la posibilidad de seguir escribiendo los mismos algoritmos que antes,
debe incorporarse la noción de función como un concepto primitivo del mismo.
Programar en un lenguaje funcional consiste en construir definiciones y,
utilizando la computadora, evaluar expresiones. El rol principal del programador
es construir una función que resuelva un problema dado. Esta función, que puede
involucrar un cierto número de otras funciones, se expresa en notación basada en
principios matemáticos normales. El rol principal de la computadora es actuar
como una “calculadora”: su trabajo es evaluar expresiones e imprimir resultados.
La diferencia con una calculadora normal es que el programador puede hacer
definiciones para incrementar su poder de cálculo.
Las expresiones que contienen ocurrencias de nombres de funciones
definidas por el programador se evalúan usando las definiciones dadas como
reglas de simplificación o reducción que convierten expresiones en formas
imprimibles.
Programación Funcional - Página 3
Paradigmas de Programación
Una característica de la programación funcional es que si una expresión
posee un valor bien definido, entonces el orden en el cual la computadora lleve a
cabo la evaluación no afecta el resultado.
Las funciones se agregan para suprimir las instrucciones.
¿Cómo construimos definiciones de funciones? Construyendo script. Un
script es una lista de definiciones.
Por ejemplo:
cuadrado z = z x z
min x y
= x, si x<=y
= y, si x>y
En este script definimos dos funciones llamadas cuadrado y min. La función
cuadrado toma un valor z como argumento y retorna el valor de z multiplicado
por sí mismo como resultado. La función min toma dos números x e y como
argumentos y retorna el valor más pequeño.
Sin prestar atención a la sintaxis de las definiciones podemos decir que están
escritas como ecuaciones entre cierto tipo de expresiones. Estas expresiones
pueden contener variables (como en el caso de x e y).
La definición introduce un binding (o ligadura) entre un nombre y un valor
dado. En el ejemplo anterior, el nombre cuadrado se asocia a la dunción que eleva
al cuadrado su argumento y el nombre min está asociado a la función que retorna
el más pequeño de sus dos argumentos.
Un conjunto de binding (o ligaduras) se denomina ambiente o contexto. Las
expresiones siempre se evalúan en el mismo contexto y pueden tener ocurrencias
de nombres encontrados en ese contexto.
El evaluador usará las definiciones asociadas con estos nombres como reglas
para simplificar las expresiones.
Hay un número de operaciones que se consideran como primitivas. Por
ejemplo, las operaciones aritméticas.
En cualquier momento es posible agregar nuevas definiciones al script.
Por ejemplo:
lado = 12
área = cuadrado lado
Si quiero evaluar el área, debería ingresar una expresión para que la
computadora evalúe y me retorne un resultado.
Por ejemplo:
ó
? área
144.
Programación Funcional - Página 4
Paradigmas de Programación
? min (área + 4) 150
148.
Resumen:
1. Los scripts son colecciones de definiciones hechas por el programador.
2. Las definiciones se expresan como ecuaciones entre un cierto tipo de
expresiones y describen funciones matemáticas.
3. Durante un sesión, se ingresan las expresiones para su evaluación. Estas
expresiones pueden contener referencias a funciones definidas en el script.
Expresiones y valores
La noción de expresiones es central en la programación funcional. Existen
muchos tipos de expresiones matemáticas, quizás no todas permiten la misma
notación, pero sí todas tienen ciertas características comunes. La característica más
importante de la notación matemática es que una expresión se usa para denotar (o
describir) un valor. En otras palabras, el significado de una expresión es su valor y
NO EXISTEN otros efectos ocultos.
El valor de un expresión depende únicamente de los valores de las
expresiones que la constituyen (si es que existen) y estas sub-expresiones pueden
reemplazarse libremente por otras que posean el mismo valor.
Una expresión puede contener ciertos “nombres” que hacen referencia a
cantidades desconocidas, pero es normal, en notación matemática, asumir que
diferentes ocurrencias del mismo nombre hacen referencia a la misma cantidad
desconocida. Tales nombres usualmente se llaman variables.
La propiedad característica de las expresiones matemáticas descripta aquí se
denomina TRANSPARENCIA REFERENCIAL.
Reducción
La computadora evalúa una expresión reduciéndola a su forma equivalente
más simple e imprimiendo su resultado. Vamos a utilizar los términos evaluación,
simplificación y reducción para describir este proceso.
Ejemplo:
Vamos a reducir la expresión cuadrado ( 3+4)
Una forma sería:
cuadrado (3+4)
ð cuadrado 7
ð7x7
ð 49
(+)
(cuadrado)
(x)
ð (3+4)x(3+4)
(cuadrado)
Otra forma sería:
cuadrado (3+4)
Programación Funcional - Página 5
Paradigmas de Programación
ð 7 x (3+4)
ð7x7
ð 49
(+)
(+)
(x)
No importa la forma que utilizamos, siempre el resultado final es el mismo.
El proceso para evaluar una expresión básicamente es simple: sustituir y
simplificar usando reglas primitivas y reglas definidas por el programador en
forma de definiciones.
Otro ejemplo: supongamos la definición recursiva de la función factorial:
fac 0 = 1
fac (n + 1) = (n+1)*fac(n)
Si reducimos la expresión fac 0
1
En un paso de computación el fac 0 se reduce a 1.
Si reducimos la expresión fac 3:
fac 3
ð
ð
ð
ð
ð
ð
ð
3*(fac 2)
3*(2*(fac 1))
3*(2*(1*(fac 0)))
3*(2*(1*1))
3*(2*1)
3*2
6
Una expresión es canónica (o está en forma normal) si no puede reducirse.
Algunas expresiones no pueden reducirse del todo. Por ejemplo, si / es la
operación de división, entonces la expresión 1 / 0 no puede reducirse. Para estos
casos existe un símbolo denominado bottom (^) que significa valor indefinido. De
esta manera, podemos decir que TODA expresión denota un valor (aunque sea ⊥).
Otras expresiones nunca terminan de reducirse:
Ejemplo: fx = f(x+1)
f2
f (2+1)
f(3)
f(3+1)
f(4)
......
Existe un orden de reducción. Las dos formas de reducción se denominan:
Orden aplicativo (o ancioso)
Orden normal (o "lazy", perezoso)
Programación Funcional - Página 6
Paradigmas de Programación
Orden aplicativo:
Aunque no necesite, debo evaluar todos los argumentos.
Orden normal
Primero resuelvo la función aunque no conozca los argumentos. No calculo
más de lo necesario.
El concepto de "lazy evaluation" surge ante la necesidad de evaluar los
argumentos de las expresiones para poder obtener los resultados. Esta técnica
consiste en que :
. La expresión no es evaluada hasta que su valor no se
necesita.
. Una expresión compartida no es evaluada más de una vez.
El primero de los conceptos es lo que se conoce en los lenguajes imperativos
como "call by name". La desventaja de este esquema es que cada vez que un mismo
parámetro formal es encontrado debe ser reevaluado. Teniendo en cuenta la
transparencia referencial, la reevaluación no tiene efectos laterales, con lo que sería
innecesaria y el resultado de la evaluación puede compartirse. Esto último es lo
que enuncia el segundo concepto y lo que transforma el "call by name" en "call by
need" (evalúa cuando lo necesita y lo hace una sola vez).
Tipos
Existen dos clases de tipos:
• tipos básicos
• tipos compuestos o derivados.
Los tipos básicos son aquellos cuyos valores son primitivos.
Por ejemplo:
num (números)
bool (valores de verdad)
char (caracteres).
Los tipos derivados o compuestos son aquellos cuyo valores se construyen
de otros tipos.
Por ejemplo:
(num, char), el tipo de pares de valores donde la primer componente
es un número y la segunda es un char.
(num->num), el tipo de funciones cuyos argumentos son números y
retornan otro número.
[num], una lista de números.
Cada tipo tiene asociado un cierto conjunto de operaciones.
TODA
tipado.
EXPRESIÓN TIENE ASOCIADO UN TIPO.
Por lo tanto, es fuertemente
Funciones y definiciones
Programación Funcional - Página 7
Paradigmas de Programación
El valor más importante en la programación funcional es el valor de una
función. Matemáticamente hablando, una función es una regla de correspondencia
que asocia cada elemento de un tipo dado A a un único miembro de un segundo
tipo B.
f: A
B
Si x denota un elemento de A, f(x) denota el resultado de aplicar la función f
a x. Este valor es el único elemento de B asociado a x por la regla de
correspondencia f.
Existe una diferencia entre el valor de un función y la definición de ella.
Para una misma función pueden existir muchas definiciones. Por ejemplo:
double x
=x +x
double’ x
=2*x
Las dos definiciones describen distintos procedimientos para obtener la
correspondencia pero double y double’ denotan la misma función.
Cuando definimos una función en un script, podemos incluir información
del tipo de la misma.
Por ejemplo:
cuadrado :: num
num
cuadrado z = z * z
Aunque si no se ingresa nada con respecto al tipo, éste puede inferirse: dado
que la operación * (multiplicación) está permitida para los números (tipo num).
Hay funciones donde el tipo de los argumentos y/o de los resultados son
generales.
Por ejemplo:
ID x = x
el tipo acá sería variable. Para describir estas situaciones, se utilizan letras griegas
para denotar variables de tipo. En este caso el tipo asignado a la función ID es
α
α.
Otro ejemplo: Tres x = 3.
El tipo asociado sería: α
Νum.
Si una expresión contiene variables de tipo, entonces decimos que denota un tipo
polimórfico.
Un sistema de tipos es el mecanismo por el cual se formaliza la introducción
de tipos en un lenguaje de programación. La motivación del uso de tipos es
producir código más eficiente, evitar errores en ejecución detectándolos en
compilación (seguridad) y ayudar al diseño separando la representación de las
operaciones (abstracción).
El concepto de polimorfismo se define en contraposición con momorfismo
donde un objeto pertenece a un solo tipo.
La propiedad más importante de un sistema de tipos en programación
funcional es que es posible determinar el tipo de una expresión sin evaluarla. Para
Programación Funcional - Página 8
Paradigmas de Programación
determinar el tipo de una expresión se infiere el tipo (o se deduce) el tipo de sus
subexpresiones, no hay tipos en el texto. La diferencia de la inferencia de tipos con
el chequeo de tipos de los lenguajes procedurales, es que éste último lo realiza el
compilador "bottom up" a partir de las declaraciones.
Los sistemas de tipos polimórficos tienen la capacidad de definir variables
de tipo que identifican genéricamente a cualquier tipo con el que se pueda
instanciar.
Ejemplo
Tres x = 3
Formas de definición
Existen diferentes mecanismos para definir funciones:
Ecuaciones simples:
Defino una función mediante una única ecuación.
Ejemplo:
cuadrado x = x * x
Análisis por casos: (guardas)
La definición consiste de expresiones de valor booleano.
Ejemplo:
min x y
= x, si x<=y
= y, si x>y.
Esta definición consiste de dos expresiones, cada una de las cuales se
distingue por expresiones booleanas llamadas “guardas”.
Otra manera de definir esta misma función sería:
min x y
= x, si x<=y
= y, otherwise (*).
Las guardas agotan todas las posibilidades y deben ser disjuntas.
(*) Ojo!!! Estoy asumiendo que existe un orden en la evaluación!!!
Definiciones locales:
Por ejemplo: f x y
= x + a,
si x>10
= x - a,
otherwise
where a= cuadrado(y + 1)
La palabra where se utiliza para introducir una definición local cuyo alcance
es la expresión del lado derecho de la definición de f.
Programación Funcional - Página 9
Paradigmas de Programación
Patrones (pattern matching):
Es posible definir funciones usando patrones sobre el lado izquierdo de la
ecuación:
Ejemplo:
count 0 = 0
count 1 = 1
count (n+2) = 2
Si hubiese utilizado n, en lugar de n+2, los patrones no serían disjuntos
porque cuando el argumento es 0 o 1, cumple también con n.
Los patrones deben ser completos, cubrir los casos y disjuntos, dos patrones
no pueden ser verdaderos al mismo tiempo.
Otro ejemplo:
And (True, x) = x
And (False, x)=False
Definiciones recursivas:
Ejemplo:
fac 0 = 1
fac (n+1)=(n+1) * fac (n)
Funciones de alto orden
Las funciones que manipulan otras funciones se dice que son "funciones de
alto orden" o "funciones de orden superior". Este concepto puede verse desde
varios puntos de vista. Ejemplo de esto es la composición de funciones. Podemos
decir que la composición es una función que toma como argumentos las funciones
a componer.
Componer f g = f.g
O, f(g)
Listas
Una lista es una colección ordenada de valores. Todos los elementos de una
lista deben ser del mismo tipo (inclusive pueden existir listas de listas).
La lista vacía se simboliza [].
La lista formada por los enteros 1 al 5 sería: [1, 2, 3, 4, 5] ó
[1..5] ó
[1, 2, ..5]
Listas por comprensión
Representan otra notación para las listas. Empleo una sintáxis adoptado de
la forma convencional para escribir conjuntos en matemáticas:
[<expresión>|<calificador>;.....<calificador>]
Programación Funcional - Página 10
Paradigmas de Programación
en donde <expresión> denota una expresión arbitraria y <calificador> es una
expresión booleana o un generador. Las formas usadas para los generadores son:
(<variable>)
<lista>
(<variable>, <variable>) <lista de duplas>
etc.
Ejemplo:
xs=[x| x
xs]
Operaciones sobre listas
- Concatenación: dos listas pueden ser concatenadas para formar una lista más
larga. Se utiliza el operador
:: [α]
[α]
[α]
Propiedades:
1) (xs ys) zs = xs (ys zs) (Prop. Asociativa)
2) [] xs = xs [] = xs
- Longitud: la longitud de una lista es el número de elementos que ésta contiene.
Se utiliza el operador #.
# :: [α]
Num
Propiedad
#(xs ys) = #xs + #ys
-
-
Head y Tail: (cabeza y cola) la función hd selecciona el primer elemento de una
lista, y tl selecciona el resto de la lista.
hd:: [α]
α
tl :: [α]
[α]
Propiedades:
1) hd([x] xs)=x
2) tl ([x] xs)=xs
3) xs=[hd xs] tl xs.
Take: La función take toma un entero no negativos y una lista xs como
argumentos. El valor de take n xs es el segmento inical de longitud n de xs (o xs
si #xs < n).
Propiedad:
take 0 xs = []
-
Takewhile: Es similar a take excepto que toman como primer argumento un
predicado. El valor de takewhile p xs es el segmento inicial de xs cuyos
elementos satisfacen el predicado p.
takewhile :: (α
Bool)
[α]
[α]
-
Map y Filter: Son funciones de alto orden. La función map aplica una función a
cada elemento de una lista
Programación Funcional - Página 11
Paradigmas de Programación
map :: (α
map f xs = [f x| x
β)
[α]
[β]
x s]
Propiedades:
Filter toma un predicado p y una lista xs y retorna la sublista de xs cuyos
elementos satisfacen p.
filter ::(α
Bool)
[α]
[α]
filter p xs = [x| x
xs, px]
Ejemplos:
impares xs = [x | x
<>0 xscubos xs = [x * x * x| x
xs, (x mod 2) <>0]óimpares xs = filter (x mod 2)
xs]ócubo x = x * x * xcubos xs = map cubo xs
Conclusiones
En la programación imperativa cuando una expresión se evalúa y el
resultado se almacena en una celda de memoria la cual es representada por el
concepto de variable.
Un lenguaje funcional no utiliza variables (como celdas de memoria) ni
sentencia de asignación (para modificar esas celdas). El paradigma funcional ve a
un programa como una función, la evaluación consiste en la aplicación de la
función.
Para describir procesos iterativos se utilizan funciones recursivas.
El valor de una expresión depende sólo del valor de sus sub-expresiones si
es que existen.
Trabajar con lenguajes imperativos utilizando funciones tiene las siguientes
dos desventajas:
• transparencia referencial: estos lenguajes permiten realizar programas que
produzcan efectos laterales.
• tipos retornados por las funciones: por lo general, tengo bastantes restricciones
con respecto a los tipos que pueden ser retornados por las funciones. Por
ejemplo, no es posible retornar una función.
Ejemplo de un lenguaje funcional: Lisp, Gopher.
En la programación funcional se decrementa la eficiencia, por los llamados a
funciones, pero se obtiene un alto nivel de programación, con menos trabajo para
el programador.
Programación Funcional - Página 12
Paradigmas de Programación
Programación funcional
Práctica
Ejercicio 1: Definir y dar el tipo de las siguientes funciones:
a) elevo_cuarta, que eleva a la cuarta su argumento. (utilice la función cuadrado);
b) max, que retorna el mayor de sus dos argumentos;
c) cinco, que dado cualquier valor devuelve 5;
d) primero, que dado un par ordenado devuelve el 1er elemento;
e) sign, que indica el signo de su argumento o 0
f) abs, que devuelve el valor absoluto de su argumento
g) xor, que devuelve el or exclusivo de sus argumentos
Ejercicio 2: Reducir de todas las formas posibles las siguientes expresiones:
a) cuadrado (cuadrado(3+7));
b) cinco (3+4);
c) abs(primero (-4, 2))
Ejercicio 3: Definir una función que determine si un año es o no bisiesto
Ejercicio 4: Definir las siguientes funciones:
a) sum: suma todos los elementos de una lista de números;
b) algunVerdadero: devuelve True si algún elemento de una lista de booleanos es
True, y False en caso contrario;
c) todosVerdaderos: devuelve True si todos los elementos de una lista de
booleanos son True, y False en caso contrario;
d) sacoDuplicados: devuelve una lista con los nombre de los mismos valores que
la original, pero eliminado todos aquellos ue fueran adyacentes e iguales.
Ejemplo:
sacoDuplicados [1,1,2,2,2,2,3,4,5,5,] sa [1,2,3,4,5]
e) cuadrados: dada una lista de números devuelve la lista de los cuadrados de
dichos números;
f) longitudes: dada una lista de listas, devuelve la lista de las longitudes.
g) pares: dada una lista de números, devuelve la lista de los elementos pares;
h) menoresDe: dados una lista de listas xss y un número n devuelve la lista de
aquellas listas cuya longitud es menor que n.
Programación Funcional - Página 13
Descargar