500 likes | 677 Views
Algoritmos y Estructuras de Datos I. Programación funcional Clase 3. Tipos recursivos El tipo definido es argumento de alguno de los constructores. Ejemplos : data N = Z | S N El tipo N tiene por supuesto al elemento Z Constructor sin argumentos
E N D
Algoritmos y Estructuras de Datos I Programación funcional Clase 3
Tipos recursivos El tipo definido es argumento de alguno de los constructores.Ejemplos : data N = Z | S N El tipo N tiene por supuesto al elemento Z Constructor sin argumentos (como False en Bool, Calor en Sensacion) El constructor S Fabrica un N a partir de otro N, agregándole una S al principio Por ahora solamente conocemos un N que es Z Podemos formar únicamente el elemento S Z Es un nuevo N Podemos usarlo con el constructor S para fabricar otro N: S(S Z) Y así sucesivamente Entonces, elementos del tipo N son Z, S Z, S(S Z), S(S(S Z)), S(S(....(S Z))...)
Tipos recursivos data BE = TT | FF | AA BE BE | NN BE Dos constructores recursivos Uno con dos parámetros del mismo tipo Tenemos dos elementos: TT y FF (constructores sin parámetros) Podemos usarlos con NN para formar valores parecidos a los de N NN(NN(...(NN TT))...) NN(NN(...(NN FF))...) O podemos combinarlos con AA AA TT FF AA FF FF O usar los BE que construimos con NN como argumentos de AA AA (NN(NN TT)) FF También combinación inversa: aplicar NN a términos que comienzan con AA NN(NN(AA TT NN(TT))) Por último, términos que contengan AA como argumentos de AA AA (AA(NN FF)(TT)) (AA(AA(NN TT) TT) FF) Este tipo también podría representar uno más conocido, ¿Cuál?
Usando recursión estructural Da una explosión combinatoria de valores posibles. Pero también, una forma de controlarla: Los dominios tienen muchos términos. Todos tienen que estar creados con uno de los constructores aplicados a cero o más argumentos. Usando pattern matching, podemos definir funciones recursivas sobre cualquier término mediante recursión estructural.
Ejemplos size :: N -> Int size Z = 0 size (S x) = 1 + size x addN :: N -> N -> N addN Z m = m addN (S n) m = S (addN n m) contarAes :: BE -> Int contarAes FF = 0 contarAes TT = 0 contarAes (NN b) = contarAes b contarAes (AA b1 b2) = 2 + contarAes b1 + contarAes b2 Para poder aplicar la función a todas las expresiones del tipo, tenemos que poner por lo menos una ecuación para cada constructor Para que el paso recursivo sea correcto, hay que usar, del lado derecho del igual, subexpresiones de la que aparece del izquierdo.
Listas Es el tipo recursivo más usado Brinda un tratamiento de secuencias. Hay muchas funciones para listas en preludio Son tipos algebraicos con sintaxis propia Primero, vamos a definirlas como un tipo algebraico común data List a = Nil | Cons a (List a) Después vamos a ver la notación de Haskell, que es más cómoda.
List a data List a = Nil | Cons a (List a) Tipo paramétrico. Instanciémoslo: List Int Tenemos Nil (sin parámetros) Lo interpretamos como secuencia vacía Podemos usar Cons Por ahora, Cons n Nil Listas unitarias Cons 2 Nil Y seguimos con Cons Cons 2 (Cons 3 (Cons 2 (Cons 0 Nil)))
Haciendo recursión estructural (List) Sumemos todos los elementos de una lista de enteros sumList :: List Int -> Int sumList Nil = 0 sumList (Cons n ns) = n + sumList ns
Sintaxis Haskell para listas (notación más cómoda) Nil se escribe [] (Cons x xs) se escribe (x:xs) Dos contructores : [] Cons 2 (Cons 3 (Cons 2 (Cons 0 Nil))) se escribe 2 : 3 : 2 : 0 : [] Notación más cómoda [2,3,2,0]
Haciendo recursión estructural (List a) sum :: [Int] -> Int sum [ ] = 0 sum (n:ns) = n + sum ns length :: [a] -> Int length [ ] = 0 length (x:xs) = 1 + length xs (++) :: [a] -> [a] -> [a] [ ] ++ ys = ys (x:xs) ++ ys = x : (xs ++ ys)
Operadores Se llama operador a una función cuyo nombre empieza con un carácter especial. Si nos referimos a la función sin aplicarla o en forma prefija, se usa entre paréntesis. Sino la usamos en forma infija entre sus operandos. Ejemplo: La función de concatenación de listas (++) Ejemplo: [1, 2] ++ [3, 4, 5]
Transparencia referencial “Quiero cambiar el primer elemento de una lista de naturales por un cuatro.” No se puede hacer, los objetos no cambian de valor: listaUno :: [Int] listaUno = [1, 2, 3, 4] “Dada una lista, quiero obtener otra que coincida con ella en todas las posiciones, excepto en la primera, que debe ser un cuatro.” Matemática: si dos listas difieren en un elemento, no son la misma ponerCuatro :: [Int] -> [Int] ponerCuatro (x : xs) = (4 : xs) No cambia ningún elemento de la lista. Crea una lista nueva parecida a la primera. Repetimos xs en el resultado para que estuviera el resto. Si aplicamos a listaUno, obtenemos [4, 2, 3, 4] El valor de listaUno va a seguir siendo el mismo Faltaría ecuación para explicar lista vacía! Que dice la especificación? Cambiar un elemento por otro. Alternativa a) lo dejamos así. (Si aplica a la lista vacía, da error: no hay elemento a cambiar por 4). Alternativa b) Nueva especificación para devolver [4] Alternativa c) devolver la lista vacía Ejemplo de importancia de contar con una especificación precisa.
Árboles Estructura formada por nodos que almacenan valores de manera jerárquica. Vamos a trabajar con árboles binarios: Cada nodo almacena un valor, y de cada nodo salen, o bien dos nodos, o bien ningun nodo. Se llama Hoja a aquel nodo del que no sale ningun otro nodo. Se llama Rama a aquel nodo del que salen otros dos nodos. Se llama Raíz a aquel nodo que no tiene padre. Típicamente los árboles se dibujan en forma invertida: Raíz arriba de todo y las Hojas abajo.
Un árbol Nodo 1 Hoja 2 Hoja 0 Nodo 3 Hoja 0
Tipo de datos Arbol a Definición del tipo data Arbol a = Hoja a | Nodo a (Arbol a) (Arbol a) El árbol del ejemplo aej :: Arbol Int aej = Nodo 1 (Hoja 0) (Nodo 3 (Hoja 2) (Hoja 0)) Funciones (recursión estructural) hojas :: Arbol a -> Int hojas (Hoja x) = 1 hojas (Nodo x i d) = hojas i + hojas d altura :: Arbol a -> Int altura (Hoja x) = 0 altura (Nodo x i d) = 1 +(altura i ‘max‘ altura d)
Transparencia referencial Función parecida a ponerCuatro. Se puede decir que cambia los números 2 de las hojas por números 3 Pero sabemos que fabrica árboles nuevos, con los mismos datos que el recibido como argumento, excepto algunos cambiar2 :: Arbol Int -> Arbol Int cambiar2 (Hoja n) | n==2 = Hoja 3 cambiar2 (Hoja n) | otherwise = Hoja n cambiar2 (Nodo n i d) = Nodo n (cambiar2 i) (cambiar2 d)
Ejercicio: Reducir cambiar2 aej Ejercicio: Reducir duplA aej y sumA aej duplA :: Arbol Int -> Arbol Int duplA (Hoja n) = Hoja (n*2) duplA (Nodo n i d) = Nodo (n*2) (duplA i) (duplA d) sumA :: Arbol Int -> Int sumA (Hoja n) = n sumA (Nodo n i d) = n + sumA i + sumA d .
Recorriendo un árbol Las funciones que vimos van “recorriendo” un árbol y operando con cada nodo. Podrían hacerlo en cualquier orden con el mismo resultado final. Sin embargo, a veces es importante el orden al recorrer un árbol para aplicarle alguna operación. Por ejemplo, buscamos en qué ubicación está la primera aparición de un dato en un árbol. La interpretación de “primera” puede cambiar. Vamos a ver dos funciones que arman una lista con los elementos de un árbol. Ambas respetan el orden de los elementos en el árbol, pero el criterio para respetar ese orden es distinto
Órdenes de recorrido de árboles inOrder, preOrder :: Arbol a -> [ a ] inOrder (Hoja x) = [ x ] inOrder (Nodo x i d) = inOrder i ++ [ x ] ++ inOrder d preOrder (Hoja x) = [ x ] preOrder (Nodo x i d) = x : (preOrder i ++ preOrder d) Ejercicio: Reducir inOrder aej y preOrder aej. Ejercicio: Definir un tercer orden: posOrder.
Aplicaciones con Conversión de tipos Convertimos el árbol en lista, de acuerdo a algún orden. Podemos aprovechar funciones sobre listas para realizar operaciones sobre los elementos del árbol. Por ejemplo podemos redefinir sumA: sumA a = sum (preOrder a)
Utilidad de Polimorfismos La mayoría de estas funciones son polimórficas (son casos más interesantes que la identidad). Estas funciones (como preOrder) se basan solamente la estructura de sus argumentos: No importa su valor. Son aplicables a muchos tipos de datos, tanto existentes como por existir.
Motivación para tipos abstractos Vimos cómo crear y cómo usar tipos de datos algebraicos. Al usar un tipo, se lo puede tratar como un tipo abstracto: Recibimos (o entregamos) un tipo de datos sin permitir acceso a representación No hace falta más de un programador. Puedo crear un tipo (como un tipo algebraico) que voy a usar yo mismo y nadie más, pero ocultar su implementación. En otras partes del programa me está prohibido accederla ¿Para qué puedo querer ponerme límites yo mismo? Despertador lejos de la cama para evitar el reflejo de apagarlo Objetos frente a la puerta de salida para no irse sin ellos En el diseño de lenguajes de programación es un punto clave. Los lenguajes deben facilitar que el programador haga lo que quiere Pero también alentarlo a hacerlo bien. Y, más importante, impedirle hacerlo mal: usar la representación interna del tipo en otros programas
Recordemos los requisitos para tipos algebraicos data Racional = R Int Int Se puede usar, pero con cuidado: Nunca construir con segunda componente 0. Funciones: mismo resultado para todas las representaciones del mismo racional Función numerador que devuelve la primera componente Resultados distintos para R (-1) (-2) y R 2 4 Son el mismo número, no estamos representando las fracciones El lenguaje puede ayudar: Recursos sintácticos para avisar que esta representación interna se usa en unas pocas funciones Mensaje de error si se intenta violar ese acuerdo.
Mantenimiento de programas Acceso por pattern matching limitado a pocas funciones Si decidimos cambiar la representación interna, el cambio se va a limitar también a esas funciones En lo posible, muy cerca una de otra en el código (mismo archivo). Si no pedimos al lenguaje esta protección Riesgo de usar (nosotros mismos u otro programador) la implementación del tipo en muchos otros programas Cuando la reemplacemos, van a dejar de funcionar todos esos programas Hasta que los modifiquemos uno por uno. Es bueno que el lenguaje brinde una manera de abstraernos de la representación interna de los tipos. Vamos a ver: Cómo usar los tipos abstractos que recibimos ya implementados. Cómo hacer para presentar como abstractos los tipos que creamos.
Uso de tipos abstractos Recibimos un tipo de datos abstracto nombre del tipo nombres de sus operaciones básicas tipos de las operaciones especificación de su funcionamiento Puede ser más o menos formal En Haskell no es chequeada por el lenguaje ¿Cómo se utiliza el tipo? A través de sus operaciones Y únicamente así No solo por convicción: no tenemos otro remedio
Ejemplo: racionales Nos dan un tipo de datos abstracto que representa los racionales No nos importa cómo fue definido. Quizás, con un tipo algebraico como vimos antes, pero no podemos aplicar pattern matching. Recibimos operaciones: crearR :: Int -> Int -> Racional numerR :: Racional -> Int denomR :: Racional -> Int Especificación Numerador y denominador: forma normalizada (máxima simplificación) con signo en el numerador. No sabemos cuándo se produce la simplificación para normalizar al construir el número en crearR al evaluarlo, en numerR y denomR numerR (crearR (-1) (-2)) == numerR (crearR 2 4) También aclara que crearR n 0 da error
Operaciones sobre racionales Tal vez nos den operaciones básicas (suma y multiplicación) También podríamos definirlas nosotros: sumaR, multR, divR :: Racional -> Racional -> Racional r1 ‘sumaR‘ r2 = crearR (denomR r2 * numerR r1 + denomR r1 * numerR r2) (denomR r1 * denomR r2) r1 ‘multR‘ r2 = crearR (numerR r1 * numerR r2) (denomR r1 * denomR r2) r1 ‘divR‘ r2 = crearR (denomR r2 * numerR r1) (denomR r1 * numerR r2)
Otro ejemplo: diccionario Guarda definiciones para ciertas palabras (como diccionario de la lengua) Visto en forma genérica (paramétrico), se usa mucho en los programas de computación. Por ejemplo, en lugar de relacionar palabras con definiciones, relaciona números de teléfonos con abonadosCombinado con un dispositivo de identificación de llamadas: averiguar el nombre de quien me llama. Almacenamiento: Para el implementador, ambos diccionarios (palabras y teléfonos) estarían fuera de la memoria Medio externo con más capacidad y tolerancia a fallas En esta materia no van a aprender cómo comunicarse con dispositivos así desde Haskell. Como programadores-usuarios no nos interesa.
Operaciones del diccionario Buscar:: Diccionario -> Palabra -> Maybe Definicion agregar::(Palabra,Definicion)-> Diccionario-> Diccionario eliminar:: Palabra -> Diccionario -> Diccionario castellano:: Diccionario Transparencia referencial agregar y eliminar no modifican, no cambian un diccionario Crean un diccionario nuevo a partir de uno existente y unos datos más. agregar (“eldiego", “jugador de futbol") castellano Construye diccionario nuevo Puedo preguntarle qué quiere decir “eldiego” castellano sigue siendo el mismo diccionario ¿Cómo se llama el nuevo diccionario? No tiene un nombre La forma de hablar de él es con la expresión entera Si queremos hacer la pregunta: buscar(agregar(“eldiego",“jugador de futbol") castellano) “eldiego" Devuelve Just “jugador de futbol"
Sobre el diccionario Además de las operaciones y sus tipos, el diseñador nos da una especificación Agregar dos veces una palabra a un diccionario es lo mismo que agregarla una vez. No importa en qué orden se agreguen dos palabras. Si se saca una palabra, su definición desaparece.
Otro ejemplo: conjuntos El tipo de datos preferido para representar colecciones en programación funcional no es conjuntos, sino listas, porque los conjuntos no pueden representarse con un tipo algebraico que cumpla las condiciones que pusimos (los constructores permiten armar de varias formas el mismo conjunto, o expresiones que no son conjuntos). La solución es crear un tipo abstracto para los conjuntos. Las operaciones disponibles para el programador-usuario van a limitar su uso como pasó con los racionales
Operaciones de conjuntos El conjunto vacío empty :: IntSet ¿El conjunto dado es vacío? isEmpty :: IntSet -> Bool ¿Un elemento pertenece al conjunto? belongs :: Int -> IntSet -> Bool Agregar un elemento al conjunto, si no estaba; si estaba, dejarlo igual insert :: Int -> IntSet -> IntSet Elegir el menor número y quitarlo del conjunto choose :: IntSet -> (Int, IntSet)
Transparencia referencial Las operaciones no modifican los conjuntos, construyen conjuntos nuevos Pero es fácil explicar las cosas en estos términos choose no quita nada El original, sigue igual que antes Devuelve un entero y otro conjunto, con todos los elementos del primero menos ese No queda claro qué pasa si el conjunto es vacío Se puede suponer que da error
Nuevas operaciones sobre conjuntos Como programadores-usuarios, definamos operación nueva: unión union :: IntSet -> IntSet -> IntSet union p q | isEmptyp = q union p q | otherwise = unionAux (choosep) q unionAux (x,p') q = insertx (union p' q) Pudimos hacerlo sin conocer la representación de los conjuntos. Usamos las operaciones provistas. Si cambia la representación el implementador tiene que rescribir las ecuaciones para las operaciones del tipo abstracto (empty, isEmpty, belongs, insert, choose) union y cualquier otra definida en otros programas quedan intactas. Observemos que la recursión no es privativa del pattern matching union está definida en forma recursiva (a través de unionAux) Pero no usa recursión estructural ¡ni siquiera conocemos la estructura!
Creación de tipos abstractos Pasemos al rol de implementadores. Escribir un tipo abstracto que puedan usar otros programadores. En Haskell se hace con módulos. Son archivos de texto que contienen parte de un programa. Por ejemplo: uno o más tipos de datos con sus funciones. O, simplemente, un grupo de funciones. Dos características importantes en cualquier lenguaje Encapsulamiento Agrupar estructura de datos con funciones básicas Programa no es lista de definiciones y tipos, son “cápsulas” Cada una con un (tal vez más) tipo de datos y sus funciones específicas Ocultamiento Cuando escribo un módulo indico qué nombres exporta Cuáles van a poder usarse desde afuera y cuáles no Aplicación: ocultar funciones auxiliares (como unionAux) También para crear tipos de datos abstractos debemos ocultar la representación interna
Ejemplo de módulo El primer ejemplo no es un tipo abstracto, sino algebraico. Vamos a agrupar la funcionalidad de los complejos y exportar el tipo completamente module Complejos (Complejo(..), parteReal, parteIm) where data Complejo = C Float Float parteReal, parteIm:: Complejo -> Float parteReal(C r i) = r parteIm (C r i) = i module introduce el módulo: nombre, qué exporta Nombre (..) exporta el nombre del tipo y sus constructores Un programador que lo use puede hacer pattern matching Después, where y definiciones Si no se pone la lista de exportación (Module Complejos where...), se exportan todos los nombres definidos
Ejemplo de módulo de tipo abstracto module Racionales (Racional, crearR, numerR, denomR) where data Racional = R Int Int crearR :: Int -> Int -> Racional crearR n d = reduce (n*signum d) (abs d) reduce :: Int -> Int -> Racional reduce x 0 = error "Racional con denom. 0" reduce x y = R (x ‘quot‘ d) (y ‘quot‘ d) where d = gcd x y numerR, denomR :: Racional -> Int numerR (R n d) = n denomR (R n d) = d
Aclaraciones signum (signo), abs (valor absoluto), quot (división entera) y gcd (gratest common divisor (MCD)) están en el preludio. En la lista de exportación no dice (..) después de Racional Se exporta solamente el nombre del tipo NO sus constructores Convierte el tipo en abstracto para los programadores que lo usen Tampoco exportamos la función auxiliar reduce.
Ahora usemos el tipo Volvemos al rol de programador-usuario . Indicar (en otro módulo) que se quiere incorporar este tipo de datos. Se usa la cláusula import Es posible importar todos los nombres exportados por un módulo (importando el nombre del módulo) O solamente algunos de ellos (aclarando entre paréntesis cuáles) module Main where import Complejos import Racionales (Racional, crearR) miPar :: (Complejo, Racional) miPar = (C 1 0, crearR 4 2) Si se pone algunas de estas expresiones: va a dar error numerR (snd miPar) reduce (crearR 4 2) R 4 2 + R 2 1
Sinónimos de tipo Nombre nuevo a un tipo de Haskell: se usa la cláusula type. No se crea un nuevo tipo, sino un sinónimo de tipo. Los dos nombres son equivalentes. Ejemplo: nombrar una instancia particular de un tipo paramétrico: type String = [Char] type IntOChar = Either Int Char O renombrar tipo existentes con nombre más significativo: type Nombre = String type Sueldo = Int type Empleado = (Nombre, Sueldo) type Direccion = String type Persona = (Nombre, Direccion) Persona es par String, pero si usamos (String, String), es difícil entender También sinónimos de tipos paramétricos: type Lista a = [a] type IntY a = (Int, a) No es una forma de definir tipos, no admite recursión
Otra forma de crear tipos Vimos cómo crear sinónimos de tipos (es un nombre adicional para un tipo existente, y resulta intercambiable) A veces queremos un tipo nuevo con la representación de uno existente, pero que NO puedan intercambiarse. Si la representación de un tipo abstracto es uno que ya existe, no usamos sinónimo: Un programador-usuario podría aprovecharlo y tomar elementos iguales como diferentes , crear valores inválidos, atar el programa a esta representación. Usamos la cláusula newtype
newtype Ejemplo, conjuntos de enteros. Los representamos internamente con listas y encerramos la representación en un tipo abstracto [1, 3, 2, 4, 3] =[1, 2, 3, 4] newtype IntSet = Set [Int] Muy similar a data, pero Llama la atención sobre renombre A otro implementador encargado de modificarla, A alguien que tenga que analizar Al mismo implementador dentro de un tiempo Admite un solo constructor con un parámetro no crea nuevos elementos renombra elementos existentes (no intercambiable) Mejor rendimiento que data
Implementación de conjuntos Representación: listas ordenadas sin elementos repetidos module ConjuntoInt (IntSet, empty, isEmpty, belongs, insert, choose) where import qualified List (insert) newtype IntSet = Set [Int] empty :: IntSet empty = Set [] isEmpty :: IntSet -> Bool isEmpty (Set xs) = null xs belongs :: Int -> IntSet -> Bool belongs x (Set xs) = x ‘elem‘ xs insert :: Int -> IntSet -> IntSet insert x (Set xs) | x ‘elem‘ xs = Set xs insert x (Set xs) | otherwise = Set (List.insert x xs) choose :: IntSet -> (Int, IntSet) choose (Set (x:xs)) = (x, Set xs)
Aclaraciones Función insert del modulo List Es útil para implementar los conjuntos Inserta un elemento en su lugar correspondiente de una lista ordenada Pero estaba definiendo una función insert para conjuntos, y no puedo tener en el mismo programa las dos funciones Uséqualified al importar insert del módulo List Para referirme a ese nombre tengo que calificarlo con el del módulo. Tengo que escribir List.insert Esto me permitió distinguir los dos insert Las funciones null (ver si una lista es vacía) y elem (ver si un elemento pertenece a una lista) están en el preludio.
Fin Terminamos el tema de definición de tipos en Haskell hasta el nivel con el que vamos a usarlos en la materia. Dejamos afuera: chiches de notación (como definir campos con nombre para los tipos algebraicos) un tema conceptualmente complejo: clases de tipos. Son extensión sobre el sistema de tipos de Hindley-Milner y agrega mucho poder más allá del polimorfismo (overloading). Pero con las herramientas que les dimos van a poder crear programas interesantes como el del proyecto de taller.
Polimorfismo Dijimos que el tipado en Haskell es fuerte Pero es común querer escribir funciones que puedan usarse, sin redefinirlas, para distintos tipos de datos Se llama polimorfismo El comportamiento de la función no depende del valor de sus parámetros Ejemplo: id (identidad): id x = x ¿De qué tipo es id? id 3, entonces id :: Int -> Int id True, entonces id :: Bool -> Bool id doble, entonces id :: (Int -> Int) -> (Int -> Int) Respuesta: id :: a -> a No importa qué tipo sea a Usamos variables de tipo id es un tipo parámetrico (depende de un parámetro) Es una función polimórfica Se lee “id es una función que dado un elemento de algún tipo a devuelve otro elemento de ese mismo tipo.”
Currificación promedio1 :: (Float,Float) -> Float promedio1 (x,y) = (x+y)/2 promedio2 :: Float -> Float -> Float promedio2 x y = (x+y)/2 promedio1 recibe como único argumento un par ordenado promedio2 recibe dos argumentos de tipo Float Separamos los tipos de los argumentos por una flecha Igual a la que separa los tipos de los argumentos del tipo del resultado No es caprichoso, tiene implicancias teóricas y prácticas (no vamos a verlas) La notación (una operación que permite) se llama currificación Por Haskell B. Curry Matemático estadounidense De su nombre también se tomó el del lenguaje que están aprendiendo Alcanza con ver que evita varios signos de puntuación (paréntesis y comas): promedio1 (promedio1 (2, 3), promedio1 (1, 2)) promedio2 (promedio2 2 3) (promedio2 1 2)
Roles Personas que trabajan en un desarrollo con tipos abstractos Una persona puede ocupar más de uno En cada momento debe tener claro en cuál está Un rol puede ser ejercido por un equipo Diseñador Establece operaciones Escribe las especificaciones Puede decidir o sugerir representación interna Implementador Escribe el código para definir el tipo, representación y funciones básicas QUÉ hacer (decisiones del diseñador) CÓMO hacerlo (código escrito por el implementador) Programador-usuario Utiliza las operaciones No sabe cómo están implementadas O sabe, pero no puede aprovecharlo Se desentiende de la implementación y usa el tipo como primitivo Ya usamos tipos abstractos (Int, Float)
Rol de los estudiantes en Algo I Prácticas y parciales Pogramadores-usuarios Usar tipos de datos que se les van a proveer Taller Además, implementadores Implementar tipos de datos abstractos Ahora veremos cómo Diseñadores Siempre los docentes de la materia Escapa al alcance de la misma Incluso, en algunos casos, al del área de Programación.