Trabajo Práctico 1 Programación Funcional

Anuncio
Trabajo Práctico 1
Programación Funcional
Paradigmas de Lenguajes de Programación — 1o cuat. 2009
Fecha de entrega: 14 de abril
1.
Introducción
Este trabajo consiste en implementar en Haskell dos intérpretes para dos lenguajes de
programación sencillos.
Estrategia de evaluación
Los dos lenguajes son muy similares entre sı́ (comparten las expresiones y los valores), pero
tienen una diferencia importante: en el primer caso la estrategia de evaluación es call-by-value,
y en el segundo call-by-name.
Ambos lenguajes trabajan con expresiones que denotan números enteros. Las posibles expresiones son: constantes enteras, operadores binarios, un condicional (ifZero), declaraciones
locales (let) y funciones de varios parámetros, posiblemente recursivas.
Utilizando una estrategia de evaluación call-by-value, las funciones reciben sus parámetros
por valor. Es decir, cada vez que se invoca una función, se evalúan primero los parámetros
reales hasta que se obtiene un valor, y recién entonces se ligan los parámetros formales a estos
valores y se evalúa el cuerpo de la función.
Con una estrategia call-by-name, las funciones no reciben los valores de los parámetros,
sino sus expresiones no evaluadas. Cuando se invoca una función, los parámetros se reemplazan textualmente en el cuerpo.
Por ejemplo, si se tiene la siguiente definición:
f(x) = x * x
La evaluación de la expresión f(2 + 3) corresponderá en cada caso a un proceso como el
siguiente:
call-by-value: f(2 + 3) → f(5) → 5 * 5 → 25
call-by-name: f(2 + 3) → (2 + 3) * (2 + 3) → 5 * (2 + 3) → 5 * 5 → 25
En el caso de las declaraciones locales ocurre algo similar:
call-by-value: let x = 2 + 3 in x * x → let x = 5 in x * x → 5 * 5 → 25
call-by-name: let x = 2 + 3 in x * x → (2 + 3) * (2 + 3) → 5 * (2 + 3) → 5
* 5 → 25
1
Técnica de implementación
Por otra parte, la implementación de los intérpretes está planteada de manera muy diferente en cada caso.
En el primer intérprete, la evaluación se plantea como un proceso recursivo, en el cual
el valor final de una expresión se calcula a partir de los valores de las subexpresiones. Para
evaluar una expresión, es necesario contar con un contexto (o entorno) que informa cuáles
son los valores de las variables. Por ejemplo, la expresión x aislada sólo tiene sentido si se la
considera en un contexto que le asigne algún valor a x.
En el segundo intérprete, la evaluación se plantea como un proceso iterativo. Dada una
expresión, se define la noción de “reducción en un paso”, que determina cómo se transforma
una expresión en otra más simple. El resultado final es el que se obtiene al reducir una
expresión tantas veces como sea necesario para transformarla en otra que no se puede reducir
más.
Notar que la estrategia de evaluación y la técnica de implementación son dos caracterı́sticas
independientes. En este TP se eligió que el lenguaje call-by-value se implemente con contextos,
y el lenguaje call-by-name mediante la noción de reducción en un paso, pero podrı́a haberse
hecho al revés.
Módulos del TP
La resolución del TP se separará en módulos, ubicados en cuatro archivos distintos:
Dict.hs: implementación de un diccionario.
Lenguaje.hs: definiciones comunes a ambos lenguajes.
ByName.hs: definiciones especı́ficas del intérprete call-by-name.
ByValue.hs: definiciones especı́ficas del intérprete call-by-value.
2.
Módulo Dict
Un diccionario se representa con una función que toma claves y devuelve Nothing (cuando
la clave no está definida) o Just v (cuando el valor asociado es v):
data Dict key val = DD (key → Maybe val)
Ejercicio 1
Sin utilizar recursión explı́cita, definir las siguientes expresiones:
a) Denota un diccionario vacı́o:
emptyDict :: Dict key val
b) Define (o redefine) el valor asociado a una clave:
extendDict :: Eq key ⇒ key → val → Dict key val → Dict key val
c) Devuelve el valor asociado a la clave:
lookupDict :: key → Dict key val → Maybe val
2
d) Elimina una clave del diccionario:
removeDict :: Eq key ⇒ key → Dict key val → Dict key val
e) Crea un diccionario a partir de una lista de tuplas (clave, valor):
makeDict :: Eq key ⇒ [(key, val )] → Dict key val
3.
Módulo Lenguaje
Las expresiones de los lenguajes con los que se trabajará denotan valores del tipo Value.
Se trabajará además con nombres de funciones (por ejemplo “factorial”) y nombres de
variables (por ejemplo “x”).
type Value = Int
type FuncId = String
type VarId = String
Una expresión del lenguaje es un valor del tipo de datos recursivo Exp.
data Op = Add | Sub | Mul
data Exp = Const Value
| Var VarId
| BinOp Op Exp Exp
| IfZero Exp Exp Exp
| Let VarId Exp Exp
| Call FuncId [Exp]
Los significados de las expresiones son los siguientes:
Const val: la constante entera val.
Var v: el valor de la variable v. Las variables se ligan en las llamadas a funciones (son
los parámetros) y en las declaraciones locales.
BinOp op e1 e2: el resultado de la operación binaria entre las expresiones e1 y e2. El
valor de op determina cuál es el operador.
IfZero e1 e2 e3: si el valor de e1 es 0, devuelve e2, y en caso contrario e3.
Let v e1 e2: devuelve el valor de e2, considerando que la variable v está ligada al valor
de e1.
Call f es: aplica la función identificada por el nombre f a los parámetros es. Para que
esto tenga sentido, la expresión debe estar acompañada de un conjunto de definiciones
de funciones (ver abajo).
Un programa es un conjunto de definiciones de funciones. Más concretamente, se representa con un diccionario que, dado el nombre de una función, devuelve la definición asociada.
La definición de una función consta de la lista de parámetros, acompañada del cuerpo de la
función.
3
type ProgramDef = Dict FuncId FuncDef
data FuncDef = FuncDef [VarId] Exp
Para que un conjunto de definiciones sea considerado un programa válido, en el cuerpo de
cada una de las funciones sólo pueden figurar libres aquellas variables que estén declaradas
en la lista de parámetros. Además, cada vez que se invoque a una función refiriéndola por
su nombre, el nombre debe estar definido en el conjunto y las aridades deben coincidir. El
criterio es similar para determinar cuándo es válido evaluar una expresión dado un conjunto
de definiciones. En general, puede asumirse como precondición las expresiones y el conjunto
de definiciones dados son válidos.
Por ejemplo, dadas las siguientes definiciones:
cubo(x) = x * x * x
sr(x, y) = let cx = cubo(x) in cx + cubo(y)
fact(n) = ifzero n then 1 else n * fact(n - 1)
El valor denotado por la expresión sr(9, 10) es 1729. El valor de fact(5) es 120. La
expresión fact(z, 10) es inválida porque z está libre, y porque la aridad no coincide con la
definida arriba. La expresión func(10) es inválida porque func no está definida como función.
Ejercicio 2
Dar el tipo y definir la función foldExp, que implemente un esquema de recursión para el
tipo de datos Exp. En este ejercicio, obviamente se puede utilizar recursión explı́cita.
4.
Intérprete call-by-value
El intérprete call-by-value se centra en una función que toma una expresión y devuelve
el resultado final. El razonamiento es recursivo. Por ejemplo, el resultado final de evaluar
ifZero M then N else O será o bien el resultado final de evaluar N o bien el resultado final
de evaluar O, dependiendo de cuál sea el resultado final de evaluar M.
Esta noción de evaluación requiere conocer también un entorno que asigne valores a las
variables:
type Environment = Dict VarId Value
Por ejemplo, si se tiene definida la siguiente función: sumar(x, y) = x + y, el resultado
de evaluar sumar(2, 3) deberı́a ser el mismo que el resultado de evaluar x + y en el contexto
que le asigna 2 a x y 3 a y.
Ejercicio 3
Definir la función
eval :: ProgramDef → Exp → Value
Dado un conjunto de definiciones de funciones y una expresión e1, eval devuelve el valor
denotado por la expresión e1.
Para este ejercicio, se permite utilizar recursión explı́cita sólo para los casos en los que sea
necesario evaluar una expresión que no sea subexpresión de la original.
4
Sugerencia: implementar primero una función auxiliar:
eval ’ :: ProgramDef → Environment → Exp → Value
que tome adicionalmente el entorno en el que se está evaluando la expresión.
Ejemplo: Si tenemos el módulo definido en la sección 6 cargado, los siguientes serian
resultados esperados:
eval programa (Call ”cubo” [(Const 2)]) debe reducir a 8
eval programa (Call ”sr” [( Const 2), (Const 3)]) debe reducir a 35
eval programa (BinOp Add (Call ”fact” [(Const 4)]) (Const 1)) debe reducir a 25
Ejercicio 4
Verificar si el intérprete escrito en el punto anterior utiliza realmente la estrategia de
evaluación call-by-value. Considerar por ejemplo las siguientes definiciones:
colgarse() = colgarse()
primero(x, y) = x
Y comprobar que primero(10, colgarse()) se cuelgue. Si el intérprete implementado
en el punto anterior no utiliza la estrategia de evaluación call-by-value, modificarlo para que
lo haga.
Sugerencia: utilizar la función seq :: a → b → b de Haskell. Dadas dos expresiones,
esta función las evalúa secuencialmente y devuelve la segunda. Es decir que, al momento de
evaluar la segunda expresión, el lenguaje garantiza que la primera fue efectivamente evaluada.
Esta función pertenece a uno de los “rincones oscuros” de Haskell, y se utiliza como
herramienta de bajo nivel para garantizar algunas cuestiones de eficiencia. Lo más importante
de este ejercicio no es la modificación al intérprete en sı́, sino observar cómo el hecho de que
Haskell utilice una estrategia de evaluación call-by-name “propaga” esta propiedad a procesos
inclusive tan complejos como un intérprete.
Vale también aclarar que no es necesario utilizar seq para implementar un proceso de
evaluación call-by-value en Haskell, pero requiere un planteo diferente al de este ejercicio.
5.
Intérprete call-by-name
El intérprete call-by-name se centra en una función que toma una expresión y la reduce
en un paso. Por ejemplo, la expresión ifZero 2 + 3 then 10 else 20 reduce en un paso a
ifZero 5 then 10 else 20. Esta expresión a su vez reduce en un paso a 20. La expresión
20 es un valor, y por lo tanto es el resultado final del cómputo.
Las reglas para reducir en un paso una expresión son las siguientes (M → N se lee “M
reduce en un paso a N ”):
Const val: es un valor, por lo tanto no se puede reducir.
5
Var v: esta expresión corresponde a un término de error, que no es un valor y no se
puede reducir. Corresponde al caso en el que figuran variables libres en una expresión
(como z + 1).
BinOp op e1 e2: pueden darse tres casos:
• Si e1 → e1’, entonces BinOp op e1 e2 → BinOp op e1’ e2.
• Si e1 es un valor y e2 → e2’, entonces BinOp op e1 e2 → BinOp op e1 e2’.
• Si e1 y e2 son valores, BinOp op e1 e2 reduce en un paso al resultado de calcular
la operación identificada por op sobre los valores e1 y e2.
IfZero e1 e2 e3:
• Si e1 → e1’, entonces IfZero e1 e2 e3 → IfZero e1’ e2 e3.
• Si e1 es un valor, la expresión completa reduce en un paso a e2 o a e3, dependiendo
del valor de e1.
Let v e1 e2: la expresión reduce en un paso a e2, sustituyendo todas las apariciones de
v por e1. Notar que en este punto no se reduce e1 porque la estrategia de evaluación es
call-by-name.
Call f es: la expresión reduce en un paso al cuerpo de la función identificada por f,
reemplazando los parámetros formales (dados en la definición) por los parámetros reales
(la lista es). Notar que no se reducen los parámetros porque la estrategia de evaluación
es call-by-name.
Para implementar las reglas de reducción, es necesario implementar primero la noción
de sustituir una variable por una expresión. Una sustitución es un mapeo de variables a
expresiones:
type Substitution = Dict VarId Exp
Ejercicio 5
Sin utilizar recursión explı́cita, definir la siguiente función:
substitute :: Substitution → Exp → Exp
Dadas una sustitución σ y una expresión e1, substitute reemplaza todas las apariciones
libres de x por σ(x), para todas las variables x en el dominio de σ.
Observación: no deben sustituirse las variables que estén ligadas por un let. Por ejemplo,
substitute sust (Let ”x” (Const 1) (Var ”x”)) siempre es igual a Let ”x” (Const 1) (Var ”x”),
sin importar cuál sea el valor de sust.
El siguiente tipo de datos representa los posibles resultados de una reducción en un paso:
data Result exp = ReducesTo exp | Done
Al reducir una expresión e1, el resultado puede ser o bien ReducesTo e1’ (i.e. e1 → e1’) o
bien Done (i.e. e1 es un valor).
6
Ejercicio 6
Sin utilizar recursión explı́cita, implementar la función:
reduceOneStep :: ProgramDef → Exp → Result Exp
Dado un conjunto de definiciones de funciones y una expresión e1, reduceOneStep devuelve
el resultado de tratar de reducir e1 en un paso.
Sugerencia: implementar primero una función auxiliar:
reduceOneStep’ :: ProgramDef → Exp → (Exp, Result Exp)
que devuelva una tupla (e1, r1), donde e1 es la expresión original.
Ejercicio 7
Sin utilizar recursión explı́cita, implementar la función:
reduce :: ProgramDef → Exp → Value
Dado un conjunto de definiciones de funciones y una expresión, reduce devuelve el valor
que resulta de aplicar sucesivamente reduceOneStep hasta llegar a una expresión irreducible.
Sugerencia: puede resultar útil la función iterate definida en el preludio.
Ejemplo: Si tenemos el módulo definido en la sección 6 cargado, los siguientes serian
resultados esperados:
reduce programa (Call ”cubo” [(Const 2)]) debe reducir a 8
reduce programa (Call ”sr” [( Const 2), (Const 3)]) debe reducir a 35
reduce programa (BinOp Add (Call ”fact” [(Const 4)]) (Const 1)) debe reducir a 25
6.
Modulo Ejemplo
module Ejemplo where
import Dict
import Lenguaje
uno = Const 1
varN = Var ”n”
varX = Var ”x”
varY = Var ”y”
add = BinOp Add
mul = BinOp Mul
sub = BinOp Sub
programa : : ProgramDef
programa = makeDict
[(”cubo”, FuncDef [”x”] (mul varX (mul varX varX))) ,
(”sr”, FuncDef [”x”, ”y”] (Let ”cx” (Call ”cubo” [varX]) (add (Var ”cx”) (Call ”cubo” [varY])))) ,
(”fact”, FuncDef [”n”] (IfZero varN uno (mul varN (Call ”fact” [sub varN uno]))))]
7
En la siguiente sección se muestra la utilización del parser que se les entrega para simplificar la creación de programas.
7.
Observaciones y funciones útiles
maybe :: a → (b → a) → Maybe b → a
iterate :: (a → a) → a → [a]
dropWhile :: (a → Bool) → [a] → [a]
Usando import Maybe:
fromJust :: Maybe a → a
Usando import Parser (provisto por la cátedra):
parser :: String → Exp
Por ejemplo: parser ” ifzero f (x) then 1 else 2 ∗ x” devuelve IfZero ( Call ”f” [Var ”x”])
(Const 1) (BinOp Mul (Const 2) (Var ”x”))
Haskell define la sintaxis para cadenas multilı́nea terminando cada lı́nea con “\” y
empezando la siguiente también con “\”. Por ejemplo:
"hola mu\
\ndo"
Para probar el funcionamiento de una función implementada en términos de foldExp, no
es necesario escribirla completa. Puede probarse una implementación parcial escribiendo
algunos de los parámetros del foldExp y completando los demás con ( error ”no implementado”).
Observar que error msg tiene tipo a para todo a. Observar también que como Haskell
utiliza una estrategia de evaluación call-by-name, un término de la forma error msg no
se evalúa a menos que se lo necesite.
Pautas de entrega
Se debe entregar el código impreso con la implementación de las funciones pedidas. Cada
función debe contar con un comentario donde se explique su funcionamiento. Cada función
asociada a los ejercicios debe contar con ejemplos que muestren que exhibe la funcionalidad
solicitada. Además, se debe enviar un e-mail conteniendo el código fuente en Haskell a la
dirección plp-docentes@dc.uba.ar. Dicho mail debe cumplir con el siguiente formato:
El subject debe ser “[PLP;TP-PF]” seguido inmediatamente del nombre del grupo sin
acentos.
El código Haskell debe acompañar el e-mail y lo debe hacer en forma de attachment
(puede adjuntarse un .zip o .tar.gz).
El código debe poder ser ejecutado en Haskell98. No es necesario entregar un informe
sobre el trabajo, alcanza con que el código esté adecuadamente comentado.
Los objetivos a evaluar en la implementación de las funciones son:
8
Corrección.
Declaratividad.
Reuso de funciones previamente definidas (tener en cuenta tanto las funciones definidas
en el enunciado como las definidas por ustedes mismos).
Uso de funciones de alto orden, currificación y esquemas de recursión.
Salvo donde se indique lo contrario, no se permite utilizar recursión explı́cita, dado
que la idea del TP es aprender a aprovechar las caracterı́sticas enumeradas en el ı́tem
anterior. Se permite utilizar esquemas de recursión definidos en el preludio o pedidos
como parte del TP. Pueden escribirse todas las funciones auxiliares que se requieran,
pero estas no pueden ser recursivas (ni mutuamente recursivas).
Importante: Se admitirá una única submisión, sin excepción alguna. Por favor planifiquen el trabajo para llegar a tiempo con la entrega.
9
Descargar