Capı́tulo 4. Definiciones de tipo 40 Definiciones de tipo Sinónimos de tipo • Un sinónimo de tipo renombra un tipo existente. X El nuevo nombre y el antiguo son totalmente intercambiables. type Entero =Integer type DeEnteroEnEntero =Entero → Entero uno :: Entero uno = 1 sucesor :: DeEnteroEnEntero sucesor x = x + 1 type ParFlotantes = (Float, Float) parCeros :: ParFlotantes parCeros = (0.0, 0.0) X Un sinónimo de tipo muy usado, definido en Prelude, es String: type String = [Char ] nombre :: String nombre = [ 0 p 0 , 0 e 0 , 0 p 0 , 0 e 0 ] Main> length nombre 4 :: Int16 :: Int nombreYApellidos :: String nombreYApellidos = ”José E. Gallardo” Main> length nombreYApellidos 16 :: Int Capı́tulo 4. Definiciones de tipo Definiciones de tipos de datos • El programador puede definir nuevos tipos. Tipos enumerados data Dı́aSemana = Lunes | Martes | Miércoles | Jueves | Viernes | Sábado | Domingo deriving Show unDı́a :: Dı́aSemana unDı́a = Lunes laborables :: [Dı́aSemana] laborables = [Lunes, Martes, Miércoles, Jueves, Viernes] • Dı́aSemana es un constructor de tipo • Lunes, Martes, . . . son constructores de datos X Un constructor de dato también puede ser simbólico, y en ese caso debe ser un operador que comience por el carácter dos puntos : X Un mismo constructor de dato no puede aparecer en dos tipos distintos en un mismo ámbito. 41 • Haskell permite usar los constructores de datos como patrones: esFinSemana esFinSemana Sábado esFinSemana Domingo esFinSemana :: Dı́aSemana → Bool = True = True = False data Dirección = Norte | Sur | Este | Oeste deriving Show girar 90 girar 90 Norte girar 90 Sur girar 90 Este girar 90 Oeste :: Dirección → Dirección = Este = Oeste = Sur = Norte data ColorSemáforo = Rojo | Verde | Amarillo deriving Show En Prelude data Bool = False | True deriving (Eq, Ord , Ix , Enum, Read , Show , Bounded ) Capı́tulo 4. Definiciones de tipo 42 Uniones • Podemos definir nuevos tipos uniendo dos tipos existentes: data LetraOEntero = Letra Char | Entero Integer deriving Show unValor :: LetraOEntero unValor = Letra 0 x 0 otroValor :: LetraOEntero otroValor = Entero 15 listaMixta :: [LetraOEntero] listaMixta = [ Letra 0 a 0 , Entero 10, Entero 12, Letra 0 b 0 ] • Cada constructor de dato tiene un tipo. Letra :: Char → LetraOEntero Entero :: Integer → LetraOEntero incLoE :: LetraOEntero → LetraOEntero incLoE (Entero n) = Entero . (+1) $ n incLoE (Letra c) = Letra . chr . (+1) . ord $ c Main> incLoE (Letra 0 a 0 ) Letra 0 b 0 :: LetraOEntero Main> incLoE (Entero 10) Entero 11 :: LetraOEntero Es incorrecto: data LetraOEntero = Char | Integer – – Incorrecto!!! deriving Show • Temperaturas expresadas en dos escalas: data Temp = Centı́grado Float | Fahrenheit Float deriving Show estáCongelado :: Temp → Bool estáCongelado (Centı́grado grados) = grados ≤ 0.0 estáCongelado (Fahrenheit grados) = grados ≤ 32.0 Capı́tulo 4. Definiciones de tipo 43 Productos • También es posible definir tipos con un único constructor y varias componentes: data Racional = Par Integer Integer deriving Show X El constructor en este caso tiene el tipo • Nombre, apellidos y edad de una persona: type Nombre type Apellido1 type Apellido2 type Edad data Persona Par :: Integer → Integer → Racional unMedio :: Racional unMedio = Par 1 2 • Usando patrones podemos definir operaciones para trabajar con racionales: numerador :: Racional → Integer numerador (Par x ) = x denominador denominador (Par :: Racional → Integer y) = y por :: Racional → Racional → Racional (Par a b) ‘por ‘ (Par c d ) = Par (a ∗ c) (b ∗ d ) Main> (Par 1 2) ‘por ‘ (Par 1 3) Par 1 6 :: Racional =String =String =String =Integer =UnaPersona Nombre Apellido1 Apellido2 Edad deriving Show pepe :: Persona pepe = UnaPersona ”José” ”Pérez” ”Sánchez” 18 • Haskell permite nombrar las distintas componentes de un tipo: type Nombre type Apellido type Edad data Persona =String =String =Integer =UnaPersona { nombre :: Nombre, apellido1 :: Apellido, apellido2 :: Apellido, edad :: Edad } deriving Show Capı́tulo 4. Definiciones de tipo • Con registros, los datos se pueden declarar con la sintaxis habitual o nombrando los campos: pepe :: Persona pepe = UnaPersona ”José” ”Pérez” ”Sánchez” 18 juan :: Persona juan = UnaPersona { nombre = ”Juan”, apellido1 = ”López”, apellido2 = ”Giménez”, edad =24 } X Además, se introducen automáticamente funciones selectoras para las componentes: Main> : t edad edad :: Persona → Edad Main> edad juan 24 :: Integer 44 • Se puede construir un nuevo valor a partir de otro existente, indicando el valor de las componentes que defieren: Main> juan {apellido1 = ”Boj”, apellido2 = ”Sanz”} UnaPersona {nombre = ”Juan”, apellido1 = ”Boj”, apellido2 = ”Sanz”, edad = 24} :: Persona • Lo cual, por supuesto, no afecta para nada al valor de juan: Main> juan UnaPersona {nombre = ”Juan”, apellido1 = ”López”, apellido2 = ”Giménez”, edad = 24} :: Persona • La siguiente función devuelve una nueva Persona con la edad incrementada: cumpleaños :: Persona → Persona cumpleaños p = p {edad = edad p + 1} Main> cumpleaños juan UnaPersona{nombre = ”Juan”, apellido1 = ”López”, apellido2 = ”Giménez”, edad = 25} :: Persona Capı́tulo 4. Definiciones de tipo 45 • Los identificadores de constructores binarios de dato también pueden ser simbólicos X Siempre han de comenzar por el carácter dos puntos data Complejo =Float : − Float deriving Show origen :: Complejo origen = 0.0 : − 0.0 parteReal parteReal (x : − :: Complejo → Float ) = x X El constructor de dato introducido tiene el siguiente tipo: (: −) :: Float → Float → Complejo Main> 1.0 : − Main> 1.0 : − (: −) 1.0 2.0 2.0 :: Complejo 1.0 : − 2.0 2.0 :: Complejo • Un constructor literal puede usarse de forma infija si se escribe entre acentos franceses: dosTercios :: Racional dosTercios = 2 ‘Par ‘ 3 • También es posible definir registros variantes • Un tipo que representa cuatro clases de figuras: type Radio =Float type Lado =Float data Figura = Cı́rculo Radio | Cuadrado Lado | Rectángulo Lado Lado | Punto deriving Show unCı́rculo :: Figura unCı́rculo = Cı́rculo 25.0 unRectángulo :: Figura unRectángulo = Rectángulo 10.0 15.0 listaFiguras :: [Figura] listaFiguras = [Cı́rculo 15.0, Cuadrado 3.0, Rectángulo 5.0 6.0] • Define dos funciones para calcular el área y perı́metro de una Figura. Capı́tulo 4. Definiciones de tipo 46 Tipos recursivos • Haskell permite la definición de tipos infinitos mediante declaraciones de tipos recursivos. undefined undefined | False indefinidoN indefinidoN :: a = undefined :: Nat = undefined – – siempre falla Los naturales I El cero es un número natural. I Si n es un número natural, entonces n + 1 también lo es. En Haskell la declaración es: data Nat = Cero | Suc Nat deriving Show Cero :: Nat Suc :: Nat → Nat • Algunas funciones uno uno dos dos :: Nat = Suc Cero :: Nat = Suc (Suc Cero) – – puede escribirse Suc uno • Cero, Suc Cero, Suc (Suc Cero), . . . los llamaremos valores definidos. • Existen otros valores del tipo que no representan ningún número natural. Main> uno Suc Cero :: Nat Main> dos Suc (Suc Cero) :: Nat Main> indefinidoN Program error : undefined X Este valor suele denotarse semánticamente como ⊥ (se lee bottom). X Se considera que las evaluaciones que producen algún error o que no terminan (divergen) se reducen a ⊥. indefinidoN =⇒ ⊥ . Main> Suc indefinidoN Suc Program error : undefined Main> Suc (Suc indefinidoN ) Suc (Suc Program error : undefined Capı́tulo 4. Definiciones de tipo • Obsérvese que dan lugar a evaluaciones distintas. Ası́, Suc indefinidoN =⇒ Suc ⊥ mientras que Suc (Suc indefinidoN ) =⇒ Suc (Suc ⊥) Suc indefinidoN está algo más definido que indefinidoN . esCero :: Nat → Bool esCero Cero = True = False esCero Main> esCero indefinidoN Program error : undefined Main> esCero (Suc indefinidoN ) False :: Bool infinitoN :: Nat infinitoN = Suc infinitoN Main> infinitoN Suc (Suc (Suc (Suc (Suc (Suc (Suc (Suc {Interrupted !} 47 • La siguiente función comprueba si un valor de tipo Nat representa un número par: esPar :: Nat → Bool esPar Cero = True esPar (Suc x ) = not (esPar x ) • El siguiente operador calcula la suma de dos valores del tipo Nat, analizando la forma del segundo argumento: infixl 6 <+> (<+>) :: Nat → Nat → Nat m <+> Cero =m m <+> (Suc n) = Suc (m <+> n) uno <+> dos =⇒ ! definición de dos uno <+> Suc (Suc Cero) =⇒ ! segunda ecuación de (<+>) Suc (uno <+> (Suc Cero)) =⇒ ! segunda ecuación de (<+>) Suc (Suc (uno <+> Cero)) =⇒ ! primera ecuación de (<+>) Suc (Suc (uno)) =⇒ ! definición de uno Suc (Suc (Suc Cero)) Capı́tulo 4. Definiciones de tipo • En general, si una función tiene más de un argumento recursivo, podemos intentar realizar la recursión sobre uno de ellos. infixl 6 <+> (<+>) :: Nat → Nat → Nat Cero <+> n = n (Suc m) <+> n = Suc (m <+> n) X No siempre es posible realizar la definición sobre cualquiera de los argumentos. • Para el tipo Nat también es posible definir el producto y la potencia: infixl 7 <∗> (<∗>) :: Nat → Nat → Nat m <∗> Cero = Cero m <∗> (Suc n) = m <∗> n <+> m – – ya que m ∗ (n + 1) = m ∗ n + m infixr 8 <↑> (<↑>) :: Nat → Nat → Nat b <↑> Cero = Suc Cero b <↑> (Suc n) = b <∗> b <↑> n – – ya que b ↑ (n + 1) = b ∗ b ↑ n 48 • Expresiones aritméticas simples sobre enteros: data Expr = Valor Integer | Expr :+: Expr | Expr :−: Expr | Expr :∗: Expr deriving Show Calcular el número de operadores que forman parte de una expresión: numOpers numOpers numOpers numOpers numOpers :: Expr → Integer =0 (Valor ) (e1 :+: e2) = numOpers e1 + 1 + numOpers e2 (e1 :−: e2) = numOpers e1 + 1 + numOpers e2 (e1 :∗: e2) = numOpers e1 + 1 + numOpers e2 • Define una función que calcule el valor de una expresión. • Define una función que calcule cuántas constantes enteras aparecen en una expresión. • Define una función que calcule el nivel de anidamiento máximo de un operador en una expresión. Capı́tulo 4. Definiciones de tipo Funciones de plegado • Para cada tipo recursivo es posible definir un recursor de plegado. • Muchas de las funciones definidas sobre el tipo Nat siguen un mismo patrón recursivo. Recordemos algunas: data Nat esPar esPar Cero esPar (Suc n) = Cero | Suc Nat deriving Show :: Nat → Bool = True = not (esPar n) (<+>) :: Nat → Nat → Nat (<+>) m Cero =m (<+>) m (Suc n) = Suc (m <+> n) X Siguen la plantilla: fun :: Nat → a fun Cero = e fun (Suc n) = f (fun n) X Función de plegado para el tipo Nat foldNat :: (a → a) → a → Nat → a foldNat f e Cero =e foldNat f e (Suc n) = f (foldNat f e n) 49 • Entonces: esPar = foldNat not True (<+>) m =foldNat Suc m Comportamiento de foldNat .. f oldN at h z (Suc (Suc (i .veces (Suc Cero)))) =⇒ h (h (i .veces . . h z)) esPar (Suc (Suc Cero)) =⇒ ! por definición de esPar foldNat not True (Suc (Suc Cero)) =⇒ ! por segunda ecuación de foldNat ... True • Escribe una función de plegado para el tipo Expr data Expr = Valor Integer | Expr :+: Expr | Expr :−: Expr | Expr :∗: Expr deriving Show Capı́tulo 4. Definiciones de tipo 50 Tipos Parametrizados (o Polimórficos) • Algunos tipos predefinidos (como las listas) son polimórficos. X Otro ejemplo: data Par a = UnPar a a deriving Show (a es una variable de tipo). Main> UnPar 1 2 UnPar 1 2 :: Par Integer Main> UnPar True False UnPar True False :: Par Bool Main> UnPar 0 a 0 True ERROR ∗ ∗ ∗ Expression ∗ ∗ ∗ Term ∗ ∗ ∗ Type ∗ ∗ ∗ Does not match : : : : : Type error in application UnPar 0 a 0 True 0 0 a Char Bool X El tipo Either data Either a b = Left a | Right b deriving (Eq, Ord , Read , Show ) listaMixta :: [Either Char Integer ] listaMixta = [Left 0 a 0 , Left 0 c 0 , Right 3, Left 0 z 0 ] otra :: [Either Bool Char ] otra = [Left True, Right 0 a 0 , Right 0 c 0 ] X El tipo Maybe data Maybe a = Nothing | Just a deriving (Eq, Ord , Read , Show ) recı́proco :: Float → Maybe Float recı́proco 0 = Nothing recı́proco x = Just (1/x ) Main> recı́proco 0.001 Just 1000.0 :: Maybe Float Main> recı́proco 0 Nothing :: Maybe Float Capı́tulo 4. Definiciones de tipo 51 Definiciones newtype • La declaración newtype crea un nuevo tipo isomorfo (con los mismos valores) a uno existente, pero con una identidad propia para el sistema de tipos. newtype Natural = UnNatural Integer deriving Show • Todo lo dicho para data vale para newtype. X newtype evita el nivel extra de indirección (causado por la evaluación perezosa) que la declaración data introducirı́a. • Las declaraciones newtype usan la misma sintaxis que las declaraciones data que poseen un único constructor con un único campo. Capı́tulo 4. Definiciones de tipo Propiedades de funciones • Podemos entender mejor el significado de un programa estableciendo propiedades de las funciones que lo forman. X Algunas propiedades pueden probarse directamente a partir de la definiciones de las funciones involucradas. • Demostrar que la composición de funciones es asociativa (.) :: (b → c) → (a → b) → (a → c) g . f = λ x → g (f x ) Asociatividad de (.) ∀ v ¦ ((f1 . f2) . f3) v = (f1 . (f2 . f3)) v ((f1 . f2 ) . f3 ) v ≡! por definición de (.) (f1 . f2 ) (f3 v ) ≡! por definición de (.) f1 (f2 (f3 v )) Transformamos ahora la parte derecha: (f1 . (f2 . f3 )) v ≡! por definición de (.) 52 f1 ((f2 . f3 ) v ) ≡! por definición de (.) f1 (f2 (f3 v )) con lo cual queda demostrada la proposición original. • Para tipos enumerados podemos probar la propiedad en cada caso not :: Bool → Bool not True = False not False = True Doble negación ∀ x :: Bool ¦ not (not x ) = x Si x es True, hay que probar not (not True) = True. not (not True) ≡! por definición de not not False ≡! por definición de not True Si x es False, . . . Capı́tulo 4. Definiciones de tipo 53 • Las declaraciones de tipo recursivas dan lugar a conjuntos infinitos. • La inducción es la técnica ideal para probar propiedades de funciones que actúan sobre tipos recursivos. • El principio de inducción para el tipo Nat: data Nat = Cero | Suc Nat deriving Show Principio de inducción para valores definidos del tipo Nat P (Cero) ∀x :: Nat ¦ P (x ) ⇔ ∧ ∀x :: N at ¦ P (x) ⇒ P (Suc x) P Cero (demostrado en caso base) ⇒ ! por paso inductivo P (Suc Cero) ⇒ ! por paso inductivo P (Suc (Suc Cero)) ¡¡las dos condiciones del principio de inducción son suficientes!! X Este principio de inducción no es válido para valores del tipo Nat parcialmente definidos (como indefinidoN e infinitoN ). Capı́tulo 4. Definiciones de tipo • Para el operador suma, (<+>) :: Nat → Nat → Nat m <+> Cero =m m <+> (Suc n) = Suc (m <+> n) Cero es neutro por la izquierda: ∀ x :: Nat ¦ Cero <+> x = x Por el principio de inducción: Caso Base: Cero <+> Cero = Cero Paso Inductivo: ∀ x :: Nat ¦ Cero <+> x = x – – HI ⇒ Cero <+> (Suc x ) = Suc x I Caso base: Cero <+> Cero ≡! por definición de (<+>) Cero I Paso inductivo: Cero <+> (Suc x ) 54 ≡! por definición de (<+>) Suc (Cero <+> x ) ≡! por hipótesis de inducción Suc x • Cuando en la proposición considerada aparece más de una variable se puede realizar la inducción sobre alguna de ellas. Asociatividad de (<+>) ∀ x , y, z :: Nat ¦ (x <+> y) <+> z = x <+> (y <+> z ) Realizando la inducción sobre la variable z : ∀ z :: Nat ¦ P (z ) donde P (z ) = ∀ x , y :: Nat ¦ (x <+> y) <+> z = x <+> (y <+> z ) Por el principio de inducción basta con probar: Caso Base: P (Cero) Paso Inductivo: ∀ z :: Nat ¦ P (z ) ⇒ P (Suc z ) • ¿Principio de inducción para el tipo Expr (49)? Capı́tulo 4. Definiciones de tipo 55 Sobrecarga • Haskell permite definir funciones monomórficas y polimórficas. X Un ejemplo de función monomórfica: not :: Bool → Bool not True = False not False = True (sólo tiene sentido para booleanos). X Un ejemplo de función polimórfica: id :: a → a id x = x Prelude> id True True :: Bool – – id con tipo Bool → Bool Prelude> (id not) True False :: Bool – – id con tipo (Bool → Bool ) → (Bool → Bool ) • Una función polimórfica tiene sentido para distintos usos. • Función sobrecargada: a medio camino entre las funciones monomórficas y las polimórficas. X Tienen sentido para más de un tipo, pero con restricciones. • Si el tipo del operador (+) fuera: (+) :: a → a → a permitirı́a 1 + 2, 1.5 + 2.5 y True + False. • Haskell utiliza un sistema de clases para modelar la sobrecarga. • Una clase es un conjunto de tipos para los que tiene sentido un grupo de funciones. X Existen distintas clases predefinidas y cada una contiene una serie de (tipos) ejemplares. • La clase Num incluye los tipos Int, Integer , Double y Float. X El modo de restringir el polimorfismo es vı́a un contexto: (+) :: Num a ⇒ a → a → a