DUF

De Wikihaskell
Saltar a: navegación, buscar
Navegación entre entregables del
DUF
Entregable 1.1 (disc.) Wadler
Entregable 1.3 (disc.)
Entregable 2.1 (disc.)
Entregable 2.4 (disc.)
Entregable 3.2 (disc.)
Entregable 3.4 (disc.)
Entregable 4.2 (disc.)
Entregable 4.6 (disc.)
Entregable 5.1 (disc.)
Entregable 5.6 (disc.)
Entregable 6.1 (disc.)

modificar este menú

En esta página se recogen las dudas planteadas en los entregables junto con sus respuestas.

Contenido

Organización por temas


Functional Programming: Why no one uses functional languages

  • ¿Podemos decir que el principal factor, por el que no ha dado la programación funcional el salto a su apogeo, es porque las características de sus sistemas no siguen la arquitectura de Von Neumann (principal arquitectura hoy día)?


Paradigma de la Programación Funcional

  • ¿Qué justifica el uso del orden de reducción aplicativo?

Esta característica permite, entre otras cosas, trabajar con estructuras de datos infinitas en los programas.

  • ¿Cómo se implementaría en Haskell una función que devuelva un número aleatorio?

Para imprimir en Haskell un número aleatorio se deben utilizar mónadas. Dado que la propiedad de transparencia referencial no nos permite que una función devuelva un resultados distinto cada vez que se llame a la misma, se necesita un mecanismo para poder utilizar números aleatorios o E/S en Haskell. Las mónadas nos permiten encapsular un dato en ellas de manera que para el sistema lo que se devuelve o recibe es una mónada de un tipo.

En nuestro caso, para imprimir en Haskell un número aleatorio podríamos utilizar el siguiente código:

import Random
aEntero :: IO Int
aEntero = getStdRandom(randomR (1, 6))
imprimeEntero :: IO()
imprimeEntero aEntero >>= print

La función imprimeEntero imprimiría por pantalla un número aleatorio entre 1 y 6. Para ello, se hace valer de la función Entero que devuelve una mónada de Entero dónde se encapsula el número aleatorio. Para que estén operativas las funciones getStdRandom y randomR se debe importar el módulo Random.

  • ¿Se pueden definir en Haskell funciones estrictas?
Para hacer que un parámetro de una función sea evaluado de manera estricta se debe hacer uso del operador $!, cuya definición es:
infixr 0 $!
($!) :: (a -> b) -> a -> b
f $! x = x `seq` f x
Este operador evalúa su segundo parámetro y posteriormente realiza la llamada a la función que recibe como primer parámetro pasándole como parámetro el que se ha evaluado anteriormente.
  • ¿Qué significa que un dato es de primera clase?

Significa que puede ser argumento o resultado de funciones o ser componente de estructuras de datos.

  • ¿Cómo es el sistema de clases de tipos en Haskell?

El sistema de tipos de Haskell contempla tres posibilidades:

  • El monomorfismo es cuando el tipo recibido o devuelto por una función o expresión está determinado usando exclusivamente las primitivas del lenguaje tales como Int, Bool,...
siguiente :: Int -> Int
siguiente x = x + 1
  • El polimorfismo paramétrico es cuando usamos expresiones de tipos que incluyen variables de tipo. Este polimorfismo no admite restricciones y cualquier tipo estándar o creado por el usuario se puede usar en lugar de la variable.
primero :: [a] -> a
primero (x:_) = x
  • Existe otra forma de polimorfismo, es la sobrecarga o polimorfismo ad hoc, en el que no todos los valores de tipo son aceptables para una variable. Haskell logra este tipo de polimorfismo introduciendo los contextos:
siguiente :: Enum a => a -> a
siguiente x = succ x
  • ¿Quién establece el orden de reducción, el lenguaje o el intérprete?

Cada lenguaje de programación presenta una determinada estrategia de reducción de expresiones, que establece el orden en que deben ser reducidas las expresiones.

  • ¿Qué características específicas diferencian a un lenguaje funcional puro de otro que no lo sea?

Un lenguaje funcional puro se caracteriza porque las funciones definibles en él, cumplen la propiedad de transparencia referencial (Una misma expresión denota siempre el mismo valor, sea cual sea el punto del programa donde aparezca), mientras que un lenguaje no puro no obliga a que todas las funciones cumplan dicha propiedad.

  • ¿Qué caracteriza a un lenguaje funcional puro no estricto y fuertemente tipado?

Un lenguaje funcional puro no estricto y fuertemente tipado se caracteriza por:

  1. Poseer transparencia referencial -> Puro.
  2. Usar un orden no aplicativo (evaluación perezosa) -> No estricto.
  3. Los elementos del lenguaje utilizables están clasificados en distintas categorías o tipos, por lo que no se permiten violaciones de los tipos de datos, es decir, dado una variable de un tipo concreto, no se puede usar como si fuera una variable de otro tipo distinto a menos que se haga una conversión -> Fuertemente tipado.
  • La evaluacion perezosa dice que una expresion se evalua solo cuando es necesario. ¿Cómo soluciona eso el problema de la duplicidad de reducciones en la reduccion normal?

La duplicidad se soluciona con la estrategia de paso de parámetros por necesidad que consiste en utilizar paso por nombre (estrategia del orden de reducción normal) y recordar los valores de los argumentos ya calculados creándose dos referencias del argumento original en vez de dos copias.

  • El polimorfismo en los lenguajes funcionales es lo mismo que en la programacion orientada a objetos?
  • ¿Una función estricta requiere forzosamente que el tipo de evaluación seleccione los redex de dentro hacia afuera?
  • ¿Qué ventajas aporta el orden de reducción normal frente al orden aplicativo?

Por un lado, el orden de reducción aplicativo puede no conducir a la forma normal de la expresión mientras que, en el orden de reducción normal, si una expresión tiene forma normal entonces al realizar la reducción también la obtendremos.

Por otro lado,el orden de reducción normal sólo reduce los redexes necesarios para calcular el resultado final,lo que nos va a permitir trabajar con estructuras de datos infinitas, algo que no ocurre en el orden de reducción aplicativo.

  • Entre el paso por necesidad, paso por valor y paso por referencia, ¿Cuál suele ser el más eficiente?

En general, el paso por necesidad evita reevaluar expresiones que se repita, por lo que puede ser más eficiente.

  • Se dice que Haskell es un lenguaje fuertemente tipado, pero a la hora de escribir una función es opcional declarar los tipos. ¿No es esto una incongruencia o el propio compilador de Haskell se encarga de habilitar los tipos de una función dependiendo del contenido de la misma?

No es incompatible ya que Haskell, además de tipificación fuerte, posee inferencia de tipos:

  • La tipificación fuerte implica que los elementos del lenguaje utilizables están clasificados en distintas categorías o tipos. Por tanto, no se permiten violaciones de los tipos de datos, es decir, dado una variable de un tipo concreto, no se puede usar como si fuera una variable de otro tipo distinto a menos que se haga una conversión. Las comprobaciones de tipo se hacen en tiempo de compilación.
  • La inferencia de tipos asigna automáticamente un tipo de datos a una función sin necesidad de que el programador lo escriba. El tipo de las funciones es reconstruido a partir de un análisis estático del programa realizado por el compilador o intérprete del lenguaje. Para ello, estudia las definiciones previas y el uso de las variables en el cuerpo de las funciones. Por ejemplo, para calcular la longitud de una lista:
longitud [] = 0
longitud (x:xs) = 1 + longitud xs

Al leer esta definición se observa que su argumento es una lista, que los valores de los elementos de la lista no influyen en el resultado y que este resultado siempre es un número (cero o el resultado de una suma). Así, el compilador deduce que el tipo de la función es:

longitud :: [a] -> Int 
  • ¿Es compatible la propiedad transparencia referencial con la posibilidad de escribir un programa dinámico que se comporte de una manera u otra dependiendo de un determinado valor?

Es incompatible si no se comporta siempre igual para un mismo valor. Por ejemplo, las operaciones de E/S podrían afectar a dicha transparencia referencial. Esta incompatibilidad se resuelve con las mónadas.

  • Dentro de las características de Haskell se introduce un nuevo concepto, "Sistema de clases de tipos", ¿a que se refiere exactamente?

Teniendo en cuenta que en haskell existen diversos tipos de funciones: funciones monomórficas (no contienen variables de tipo y sólo pueden actuar sobre parámetros de un tipo fijo), polimórficas (contienen una o más variables de tipo y pueden actuar con cualquier tipo que se obtenga al sustituir las variables de tipo por tipos concretos) y sobrecargadas (se encuentran entre las otras dos anteriores y tienen sentido para más de un tipo, pero no para cualquiera).

Bien, pues el sistema de clases de Haskell permite restringir el tipo de ciertas funciones polimórficas imponiendo condiciones a los tipos usados en su declaración

  • Dado que existe el orden aplicativo y el orden normal, y cada uno tiene sus inconvenientes, ¿ es posible que se pueda utilizar indistintamente un orden que otro a cada paso que damos en la reducción, o si nos decantamos por un orden, debemos seguirlo hasta el final?

No se pueden utilizar los dos órdenes a la vez. Si haces tú la reducción sí que eres capaz de saber cuándo te conviene más un orden u otro, pero no puedes hacer que el intérprete o compilador elija a medida que reduce.

  • ¿Qué es el Prelude en Haskell (hugs)?
Es un módulo estándar importado por defecto en todos los módulos de Haskell. Prelude
  • Se dice que la programación funcional puede verse como una evolución de otros paradigmas. ¿En que paradigmas esta basada la programación funcional y que toma de ellos?

La programacion funcional es un paradigma de programación declarativa basado en la utilización de funciones aritméticas que no maneja datos mutables o de estado. Enfatiza la aplicación de funciones, en contraste con el estilo de programación imperativa, que enfatiza los cambios de estado. La programación funcional tiene sus raices en el cálculo lambda, la aplicación de las funciones y la recursión. Muchos de los lenguajes de programación funcionales pueden ser vistos como elaboraciones del cálculo lambda.

  • ¿Para qué se utilizan las estructuras de datos infinitas en el orden normal?
  • ¿Durante la evaluación perezosa siempre se almacenan los cálculos ya realizados? ¿Puede esto llegar a ser ineficiente?
  • ¿Una función en forma normal siempre se puede evaluar?
Si se puede evaluar siempre que no contenga algún error, lo que no se garantiza es que se encuentre una expresión en forma normal, ya que hay funciones y expresiones que no tienen dicha forma. Como por ejemplo:
infinito :: Integer
infinito = 1+infinito
Esta expresión estaría continuamente evaluándose pero siempre se volvería a llamar a ella misma y nunca acabaría.
  • Según la wikipedia, el "estándard" Haskell 98 sólo es una versión mínima, estable y portable del lenguaje, acompañado de una librería básica base para futuras extensiones. Si ésto era en el 98, ¿por qué 11 años después todavía no se ha conseguido su esperado sucesor? ¿Qué es lo que hace tan difícil estandarizar un lenguaje funcional?
  • Una de las características del lenguaje funcional es la Transparencia Referencial, ¿Podríamos emularlo en lenguajes imperativos?

No, ya que en los lenguajes imperativos existen los llamados efectos laterales de forma que el resultado de evaluar una expresión compuesta no dependerá únicamente del resultado de evaluar las subexpresiones que la componen, como ocurre en los lenguajes funcionales, sino que dependerá también de la historia del programa en ejecución y del orden de evaluación de las subexpresiones que la componen.

Ejemplo aquí: Transparencia Referencial

  • ¿En que consiste la transparencia referencial?

Puedes consultarlo en el siguiente enlace Transparencia Referencial

  • Si los lenguajes funcionales vienen a solucionar la crisis del software, ¿Por qué implementan tecnologías como las monadas y/o otras vias de escape a la programación imperativa?¿Se puede decir que la PF no ha cumplido su objetivo (pesé a ser útil)?

  • Se habla de clases de datos y de tipos de datos, habría que aclarar un poco las diferencias entre ambos conceptos.

- Un tipo es una colección de valores relacionados. Un ejemplo de tipo es el de los valores booleanos: Bool. El tipo Bool tiene dos valores True (verdadero) y False (falso).

- Una clase es una colección de tipos junto con ciertas operaciones sobrecargadas llamadas métodos. Un ejemplo de clase es Eq, que contiene los tipos cuyos valores son comparables por igualdad.

  • ¿Por qué mediante la reducción normal no se llega a la forma normal de la expresión infinito cero?

Porque la expresión infinito cero no tiene forma normal y la reducción normal llega a la forma normal de la expresión siempre que ésta exista.

  • ¿Cuál es la diferencia exacta entre entre paso por nombre y orden normal? Por ejemplo si tenemos:
     func :: Integer → Integer → Integer
     func x y = x

y la llamamos con:

func expresion1 expresion2

¿en orden normal se evaluarían ambas expresiones y en paso por nombre sólo la primera?

Haskell


Conceptos fundamentales

  • ¿ Se conoce la diferencia entre el rango de valores que manejan los tipos "double" y el tipo "float"?
  • ¿Se puede definir un operador y una función con el mismo nombre?
  • ¿Por qué no se pueden definir operadores unarios en Haskell?

El único operador unario en Haskell es la negación -, y aún así no es posible usarla en muchas situaciones, teniendo que ser reemplazada por la función subtract o negate según el caso. Las posibles ambigüedades a las que llevaría el uso de operadores prefijos es la causa más probable de que decidieran no incluir esta opción en Haskell. De hecho, hay una corriente de programadores que preferiría que desapareciera también el operador -.

  • ¿Cuál puede ser el motivo de que las funciones del tipo Char, siendo éste un tipo de dato simple, no estén incluidas en el Prelude?
  • Me gustaría ver un ejemplo de definición y uso de un operador especial

Un ejemplo de definición de un operador sería definir el operador (~=) para ver si dos valores son aproximados. Su definición se realizaría de la siguiente manera:

infix 4 ~=
(~=) :: Float -> Float -> Bool
x ~= y = abs (x - y) < 0.0001 

Dicho operador se ha declarado no asociativo con prioridad 4.

A continuación dos ejemplos de utilización del operador (~=):

 Hugs> 1.0 ~= 2.0
 False
 Hugs> 1.0 ~= 1.00001
 True
  • ¿Se puede definir una "función como operador", pero que actúe sobre más de dos operandos?

No, ya que los operadores al tener que declararlos con notación infija, necesitan actuar sólo sobre dos operandos.

  • En las funciones definidas a trozos, si no se cumple ninguna “guarda”, y tampoco está la guarda “otherwise” ¿cómo reacciona el programa?

El intérprete muestra el fallo Program error: pattern match failure indicando que no se ha encontrado un patrón que encaje con el parámetro de entrada.

  • ¿Siempre hay que indicar la prioridad de los operadores creados por uno mismo? En caso contrario, ¿cuándo hay que indicarlo?

No tiene porque indicarse la prioridad, si no se indica nada se le asigna prioridad 9 y asociatividad por la izquierda.

Si se decide indicar la prioridad y la asociatividad se tiene que seguir el siguiente formato.

   infix prioridad identificador del operador (define un operador no asociativo)
   infixl prioridad idenficador del operador (define un operador asociativo por la izquierda)
   infixr prioridad identificador del operador (define un operador asociativo por la derecha)

La prioridad puede oscilar entre 0 y 9, siendo 9 la prioridad máxima. Existe prioridad 10 pero está reservada para las funciones.

Si no se desea indicar la prioridad simplemente no se indica. Por ejemplo

   infixr :<<<

Se crea el operador asociativo por la derecha y con asociatividad 9.

  • ¿Cuál es la diferencia de usar where o let e in en una expresión?

Las expresiones let son útiles cuando se necesitan un conjunto de declaraciones locales. Por ejemplo:

let y = a * b
    f x = (x + y) / y
in f c + f d

El conjunto de definiciones creada por una expresión let es mutuamente recursiva, y la comparación de patrones es tratada en forma perezosa. La únicas declaraciones permitidas son declaraciones de tipos de funciones, enlace de funciones, y enlace de patrones.

Sin embargo, a veces se necesitan declaraciones locales en una ecuación con varias con guardas, y ésto lo conseguimos con la cláusula where.

f x y |  y > z   =  ...
      |  y == z  =  ...
      |  y < z   =  ...
     where z = x * x

Ésto no puede hacerse con let, ya que sólo abarca a la expresión que contiene. Las cláusulas where y las expresiones let tienen las mismas propiedades y restricciones sobre las ligaduras.


  • En Haskell, si no se especifica lo contrario, con ayuda de las cláusulas (infix, infixl e infixr), los operadores toman por defecto prioridad 9 y asociatividad por la izquierda. ¿Es posible cambiar estos valores por defecto, si se modifica el modulo Prelude?
  • Una vez realizada una colección de funciones y una función principal como podemos generar un ejecutable para poder realizar una aplicación sencilla en Hugs?
  • ¿Cuál es la diferencia entre case y las funciones a trozos?, creo que con ambas sentencias se pueden obtener los mismos resultados, en ese caso,¿ es posible que sea más eficiente usando un método que otro?.
  • Con la cláusulas where podemos declarar variables locales a una función. Si tenemos varios módulos y uno de ellos importa otro módulo, si queremos que una variable sea local al módulo entero, pero no se exporte al módulo del que es llamado, ¿cómo podriamos hacerlo?
  • ¿Porqué los calculos con datos de tipo Integer son menos eficientes que con datos de tipo Int si los Integer tienen una precisión ilimitada?

Precisamente. Los valores del tipo Int son números enteros de precisión limitada que cubren al menos el intervalo [-2^29, 2^29 - 1] (Los valores exactos se pueden obtener con minBound y maxBound puesto que Int es una instancia de la clase Bounded), mientras que los valores del tipo Integer son números enteros de precisión ilimitada. Esto significa que el compilador (o intérprete) conoce previamente el espacio necesario para almacenar un Int, no así un Integer. Pero la eficiencia no se encuentra sólo en el almacenamiento, sino que de esta forma si se usa el tipo Int estamos simplificando el acceso a estructuras y cálculos, lo que nos da un servicio más rápido.

  • ¿No hay ninguna manera de comparar con los operadores de igualdad y de orden valores de distintos tipos?

Sí se puede hacer. En el caso del operador de igualdad, hay que declararar una instancia de la clase Eq para ese tipo con el cual queremos utilizar el operador. En el caso de los operadores de orden, hay que declarar una instancia de la clase Ord para el tipo.

  • ¿Qué sentido tiene utilizar la función otherwise si siempre devuelve el valor boolenano True?

No siempre devuelve True, lo que si es cierto es que otherwise es equivalente a True, es decir, siempre va a ser verdadera la guarda, por ello la otherwise debe situarse al final ya que si no no seguiría comprobando guardas. Con los siguientes ejemplos se verá la diferencia.

  signo :: Integer -> Integer
  signo x
       | otherwise = -1
       | x > 0 = 1
       | x == 0 = 0

Main> signo (10)

-1

  signo :: Integer -> Integer
  signo x
        | x > 0 = 1
        | x == 0 = 0
        | otherwise = -1

Main> signo (10)

1


  • ¿Qué significan los operadores !!, ^^, $ y $!?

El operador !! toma como parámetros una lista de cualquier tipo y un entero, y devuelve el elemento de la lista de la posición dada en el segundo parámetro. Se empieza a contar desde 0

 Hugs> [1,2,3,4] !! 3
 4

El operador ^^ toma dos parámetros y devuelve la potencia, tomando como base el primero, y como exponente el segundo, a diferencia de ^, este operador puede tomar como exponente un entero negativo.

 Hugs> 2 ^^ (-1)
 0.5

El operador $ fuerza la evaluación perezosa, la función tiene está definida así:

 ($) :: (a -> b) -> a -> b
 f $ x = f x

En principio puede parecer redundante la aplicación de este operador, pero al tener asociatividad a la derecha, puede permitirnos eliminar paréntesis, por ejemplo:

 f $ g $ h x  =  f (g (h x))

El operador $! fuerza la evaluación estricta, está definida como sigue:

 ($!) :: (a -> b) -> a -> b
 f $! x = x `seq` f x


  • ¿Podría indicar un ejemplo de uso del operador ##?
  • En el ejemplo de uso de la expresión where, se ve que se pueden realizar varias definiciones locales a la vez, ¿cómo se realizaría una definición múltiple usando let...in?

Una definición múltiple usando where sería la siguiente:

 ejemplo x y = r + s
         where r = 21*x
                    s = 6*y

La equivalente usando let...in sería:

 ejemplo x y = let r = 21*x 
               s = 6*y
               in  r + s
  • ¿Existen funciones equivalentes a toUpper, toLower para el tipo string?

No, las funciones toUpper y toLower están definidas para el tipo char, pero haciendo uso de la función map podríamos aplicarlo a una lista. Por ejemplo: 'map toUpper l', pasaría la lista l a mayúscula y 'map toLowe l' pasaría la lista l a minúscula

  • ¿El sangrado de las funciones definidas a trozos es obligatorio para que se puedan ejecutar o por motivos estéticos para facilitar la lectura del código?

En Haskell la disposición del texto del programa (el sangrado) delimita las distintas definiciones mediante la regla del fuera de juego (layout o offside rule): una definición acaba con el primer trozo de código con un margen izquierdo menor o igual que el del comienzo de la definición actual. Si los trozos tuvieran el mismo sangrado que el comienzo de la definición de la función, entonces no formarían parte de dicha definición así que es obligatorio.

  • ¿Por qué al ejecutar la función "listainfinita", el intérprete comienza a escribirla directamente, y no hace un error de overflow de pila (como harían otros lenguajes)?
  • ¿Cual es la mejor fuente para localizar librerías de Haskell?

URL --> http://hackage.haskell.org/packages/archives/pkg-list.html

  • ¿Qué significa que String es un sinónimo del tipo Char?¿Cómo se implementa esa categoría de relación?

Esto quiere decir que String está definido utilizando la palabra clave type, es decir String está definida así:

type String = [Char]

Para poder definir sinónimos, haremos uso de la palabra type, y podremos utilizarlo de la siguiente manera:

type Sinonimo = Tipo

  • ¿Existe alguna forma de definir tipos nuevos o de renombrar una composición de tipos? Ésta pregunta tiene relación con la facilidad de programación y legibilidad del código. Ya que si por ejemplo, tenemos que una función recibe como argumento una tupla:
  • ¿Cómo funcionan los operadores predefinidos >> , >>=, =<<, $ y $! ?
  • Para usar las funciones del tipo Char hay que importar el módulo de biblioteca Char, ¿hay que importarlo cada vez que se usen dichas funciones o hay alguna manera de que se quede importado una vez usado por primera vez?

Una vez esté cargado el módulo Char, no hace falta volverlo a importar, al igual que ocurre cuando hacemos uso de las funciones que definimos en nuestros ficheros .hs

  • ¿Que intervalo de los números reales comprende el tipo Float? ¿y el tipo Double?

Ambos cumplen el estándar de la IEEE para aritmética en coma flotante (IEEE 754) [1] La diferencia entre Float y Double radica en la precisión:

float: representan números en punto flotante de precisión simple
double: números en punto flotante de precisión doble, presentando esta una mejor aproximación.

El rango de cada uno dependerá del compilador/interprete y de la plataforma en que se ejecute. Los valores exactos se pueden obtener con minBound y maxBound.

  • Significado de los siguientes operadores predefinidos: !! ÎÎ 'quot' 'rem' $ $! 'seq'
  • ¿ Cuál es la diferencia de intervalos entre el tipo "double" y el tipo "float"?
  • ¿Por qué Haskell no incluye en su sintaxis los numeros negativos?
  • ¿Cómo especificaríamos que una función recibe una lista?

En la definición de la función escribiríamos el tipo general de los elementos de la lista, entre corchetes. Un ejemplo

    • funcion::[Char] -> [Char] --> Esto es una funcion que recibe una lista de caracteres, y devuelve una lista de caracteres.
    • funcion::[Integer] -> [Integer] --> Duncion que recibe una lista de enteros, y devuelve una lista de enteros.
  • ¿Es posible cambiar la prioridad a una función?

En Haskell, las funciones tienen la prioridad máxima, que es 10, por lo que están todas al mismo nivel y por tanto pienso que no es posible cambiar la prioridad de las funciones en si. Ahora bien, lo que podríamos hacer, es declarar la función que deseamos que tenga menos prioridad como un operador, por lo qué podríamos declararle abiertamente la prioridad, así como la asociatividad; claro esta, que solamente podríamos hacer esto con funciones que reciban dos argumentos.

  • ¿Existe alguna otra manera de que una función devuelva más de un valor además de las tuplas o las listas?

Si. Podriamos utilizar un nuevo tipo definido por el propio usuario que permita devolver varios valores, para ello podriamos servirnos de los productos y podriamos utilizar algo así:

data VariosValores a = Valores a a a deriving Show

De esta manera podriamos crear funciones que devuelvan 3 valores simultaneamente de cualquier tipo.

  • Las declaraciones de tipo son opcionales. ¿Hay alguna ventaja al escribirlas aparte de la claridad?

Al escribir las declaraciones de tipo, añadimos un mecanismo de comprobación que hace que Haskell chequee los tipos, pudiendo evitar errores que de otro modo pasarían desapercibidos.

  • ¿Se pueden comparar dos tuplas con tipos idénticos?

Sí, ya que el operador == permite realizar dichas comparaciones:

Hugs> (3, 3) == (3, 3)
True
Hugs> (3, 4) == (4, 5)
False
Hugs> ('a', 'b') == ('a', 'b')
True
Hugs> ('d', 'a') == ('a', 'b')
False
  • ¿Se pueden sobrecargar los operadores para que actuen de forma diferente según el tipo de dato?

Sí, podemos declarar una instancia de una clase Eq por ejemplo, y cambiar el significado de (==).

  • ¿Existen funciones definidas para el tipo string que no estén definidas para el tipo char o viceversa?

Sí, por ejemplo la función words sólo funciona para String, y no para char.

  • En la sobrecarga y por soportar la inferencia de tipos… ¿Avisa Haskell en tiempo de compilación de posibles conflictos en funciones donde no se declaren los tipos?
  • ¿Se puede cambiar el tipo de un componente de una tupla en tiempo de compilación?
  • ¿Es posible definir operadores con un número de parámetros distinto de dos?

No, en Haskell solo existe un único operador unario que es la negación y ya está definido en el sistema. Los que defina el usuario deben ser todos binarios.

  • ¿Qué significan los operadores predefinidos !!, $ y $!?

El operador !! selecciona de una lista un elemento a partir de su posición. Es equivalente a la función at de otros lenguajes de programación.

 Hugs>[1,2,3,4,5] !! 3
 4

El operador $ se utiliza para forzar la evaluación de todo lo que se encuentra a su izquierda en una expresión antes de evaluar lo que se encuentra a su derecha, pues su prioridad es cero. Equivaldría a poner paréntesis a toda la parte izquierda de la expresión. Se usa, sobre todo en combinación con el operador composición (.)

f.g.h$x es equivalente a (f.g.h)x

El operador $! lo contrario que $. Fuerza la evaluación de todo lo que se encuentra a su derecha antes de evaluar lo que se encuentra a su izquierda. En este caso equivale a algo más que poner paréntesis a la parte derecha de la expresión, pues el poner paréntesis a la derecha de un operador no fuerza la evaluación del paréntesis. Se utiliza para implementar la evaluación impaciente en los casos que se necesiten.

  • ¿Cómo se devuelve una tupla infinita?
  • ¿Existe alguna guia de estilo para Haskell?

No existe una guía oficial para Haskell pero sí alguna que otra página que da consejos sobre el estilo a seguir.

  • Si no se indica la prioridad, ¿cuál es la prioridad por defecto asignada a un operador?

Cuando definimos un operador no estamos definiendo mas que una función a la cual se permite llamar de manera infija, por lo tanto su prioridad será la máxima (prioridad 10)

  • Se definen a los operadores como funciones con 2 parámetros. Sin embargo he podido definir y utilizar un operador con un solo parámetro de forma prefija. Existe alguna forma de utilizarlo de forma infija?

La notación infija es adecuada para operaciones binarias. Aunque se puede utilizar para operaciones condicionales en otros lenguajes, como el C. (<expr>?C1:C2).

  • Si el operador && tiene evaluación perezosa, por qué cuando incluyo una función infinita como primer operando da un stack overflow?

Porque aunque la evaluación sea perezosa, esta evaluación se realiza, en primer lugar evaluando los operandos más externos y en caso de igualdad, de izquierda a derecha, con lo cual si utilizamos la siguiente sentencia.

funcionInfinita && False

aunque el resultado de esta sentencia debiera ser False, al evaluar primero el operando de la izquierda se produce el stack overflow. Si por el contrario hubieramos usado esta sentencia:

False && funcionInfinita

entonces no hubieramos tenido dicho problema, y la sentencia se hubiera evaluado como falsa.

  • ¿Qué patrón se reconoce primero?¿El que concuerde primero o el que concuerde más extensivamente?

El patrón que concuerde primero es el que siempre se toma.

  • Se dice que Haskell tiene inferencia de tipos. Imaginemos que declaramos una función sin declarar los tipos y a esta función le pasamos un valor entero. ¿A qué tipo infiere Haskell, a Integer o a Int? Lo mismo pasaría si le pasamos un valor real: ¿inferiría a Float o a Double?
  • ¿Se pueden cambiar la prioridad de los operadores predefinidos?

Sí, ver la siguiente duda:

¿Siempre hay que indicar la prioridad de los operadores creados por uno mismo? En caso contrario, ¿cuándo hay que indicarlo?
  • ¿Permite Haskell sobrecarga de todos los operadores aritméticos?Es decir, ¿podemos hacer que el operador '+' de suma se sobrecargue de manera que actúe tal y como queremos?
  • ¿Ya que Haskell es un lenguaje funcional, y la característica especial que tienen es que permiten construir aplicaciones con propósito principalmente matemático, por qué incluye un sólo operador unario, en su caso la negación?¿Existe un déficit del lenguaje en cuanto al número de operadores y funciones disponibles?
  • ¿Por qué Haskell no implementa nativamente los enteros negativos?
  • ¿Es posible hacer operaciones entre tuplas? Por ejemplo (1, 2) - (1,1) = (0,1)

Es totalmente posible realizar operaciones entre los elementos de una tupla. Los elementos de una tupla pueden ser usados como si fueran elementos normales y realizar operaciones con ellos.

En el siguiente ejemplo se muestra una función que recibe dos tuplas, con el tipo de sus elementos sin definir, y una función para realizar operaciones con estos elementos.

      operacionesTuplas::(a,a) -> (a,a) -> (a->a->a) -> (a,a)
      operacionesTuplas (x,y) (v,w) f = (f x v, f y w)

Ejemplos de uso de esta función es el siguiente.

Suma:

     Main> operacionesTuplas (1,2) (3,4) (\n m -> n + m)
     (4,6)

Y el ejemplo propuesto en la duda:

     Main> operacionesTuplas (1,2) (1,1) (\n m -> n - m)
     (0,1)      
  • ¿Posee Haskell un operador del tipo puntero, o algo parecido?

En Haskell no hay punteros, además no habría que preocuparse de ellos, ya que , el Garbage Collector se ocuparía de ellos. El programador puede preocuparse de la implementación del algoritmo, no de administración de memoria.

  • Imagino que la entrada/salida se hará mediante funciones de una libreria. ¿Cuales podemos emplear y donde podemos obtenerlas?
      Haskell no realiza la entrada/salida mediante funciones
      sino mediante mónadas.
      El problema que tiene Haskell con las funciones de
      entrada/salida es la perdida de transparencia referencial.
      Por ejemplo, la función:
    
                leerEntero :: Integer
      no tomaría ningún argumento y devolvería el número leído.
      Esta función no sería pura ya que puede devolver valores
      distintos cada vez que es invocada  (el número que el
      usuario introduce).
      Haskell soluciona esto realizando la entrada/salida
      mediante   mónadas.  Haskell tiene definido el tipo
      polimórfico IO y debe ser entendido como IO a es el
      tipo de una acción que puede realizar operaciones de
      entrada/salida y que cuando es realizada genera un
      resultado de tipo a.
      Por ejemplo:
                getChar :: IO Char
      no devuelve un carácter sino una acción y cuando se
      efectúa esa acción genera una carácter. Por tanto esta
      función siempre devuelve lo mismo  “la acción de leer
      un carácter de teclado” y no el carácter leído.
  • ¿Existe un operador de concatenación para el tipo Char que devuelva un tipo string o sólo es posible concatenar haciendo ['H', 'o', 'l', 'a'] o 'H':'o':'l':'a':[]?
El operador ++ concatena dos listas devolviendo una lista. En Haskell una cadena es una lista de caracteres.
Main> ['h','o','l','a'] ++ [' ','a', ' '] ++ ['t','o','d','o','s']
"hola a todos"
  • En haskell, ¿están las funciones obligadas a devolver algún dato? ¿Se pueden definir funciones que devolvieran un tipo "void"?
  • ¿Puedo declarar un operador sin usar símbolos sino sólo letras como si fuera una función pero emplearlo como un operador?
Si, se puede realizar, a esto se le considera convertir una función en un operador. Para que sea posible es necesario que dicha función tenga 2 parámetros. Para crearla hay que escribir el nombre de esta entre acentos franceses (Botón del teclado a la derecha de la letra P). Por ejemplo, podemos definir la función suma como operador del siguiente modo:
suma :: Integer -> Integer -> Integer
x `suma` y = x + y
De este modo se podría llamar a la función de esta forma:
1 `suma` 2

  • ¿Por qué haskell tiene operadores como ÎÎ, que no son símbolos normales?
  • ¿Por qué Haskell admite que sus operadores funciones de forma prefija e infija y no postfija?
  • ¿Cómo se resolvería el siguiente ejercicio que se encuentra en la bibliografía?

Defina una función aEntero::[Integer]->Integer que transforme una lista de dígitos en el correspondiente valor entero. Main> aEntero[2,3,4] 234::Integer

    aEntero :: [Integer] -> Integer
    aEntero [x] = x
    aEntero (x:y:z) = aEntero ((10*x+y):z)
  • ¿Cuándo haría falta utilizar los operadores de conversión de tipos fromInt :: Int -> Float o fromInt :: Integer -> Float? No consigo encontrar un ejemplo para su uso, ya que las expresiones como 1 o 3 se pueden utilizar sin ningún tipo de problema
  • El constructor de tipo permite que el resultado de una función pueda ser otra función, ¿alguien me puede poner un ejemplo de este uso?
  • En las definiciones locales que sean funciones,¿se puede declarar el tipo de éstas en la clasula where?
  • Mi duda surge al estudiar las definiciones locales en Haskel,utilizando la palabra where.

En el ejemplo de la función raices de de la trasparencia de clase, que vemos a continuación, se define raiz y discriminante en el orden que se ve:

raices :: Float -> Float -> (Float, Float)
raices a b c 
 | discriminante >= 0 = ((- b + raiz) / (2 * a), 
                         (- b - raiz) / (2 * a))
 | otherwise = error "raíes complejas"
 Where raiz = sqrt discriminante
       discriminante = b ^ 2 - 4 * a * c


Mi pregunta es la siguiente: ¿Da igual el orden de la definiciones locales o hay que seguir el orden de utilización dentro de la función?

  • ¿Existe la posibilidad de definir a trozos una expresión lambda?
  • ¿La diferencia entre utilizar el patrón subrayado o no, (x:xs) o (_:xs), solo radica en el buen estilo de programación, o difieren tambien en eficiencia?.

También en eficiencia, ya que con el patrón subrayado no se evalua la expresión, por ejemplo si tuviéramos una lista de funciones, habría que ir evaluando las funciones

  • ¿Pueden utilizarse, como patrón aritmético, por ejemplo, una expresión lamba, o incluso una función previamente definida por nosotros?.
  • ¿Haskell ofrece la implementación de otros patrones aritméticos distintos del (x+k)?

No. Existen otros tipos de patrones (Constantes, pseudónimos, para listas o subrayado) pero el patrón aritmético está definido como (n+k), donde k es una constante natural, sólo unifica con un parámetro que sea un número entero mayor o igual que k y asocia a la variable n el valor de dicho número entero menos k.

  • ¿Se pueden extender los patrones lista para obtener formas como, por ejemplo (x:xs:z), o algo parecido?

Si. El patrón que se indica en la pregunta haría que la x quedara ligada al primer elemento de la lista, xs al segundo elemento y z sería equivalente al resto de la lista. En el siguiente ejemplo:

sumaTresPrimeros :: [Integer] -> Integer
sumaTresPrimeros (x:y:xs:z) = x + y + xs

realizamos la suma de los tres primeros elementos de una lista de enteros y descartamos el resto. Si la lista tiene menos de 3 elementos mostraría un mensaje de error.

Main> sumaTresPrimeros [4, 5, 6]
15
Main> sumaTresPrimeros [4, 5]

Program error: pattern match failure: sumaTresPrimeros [4,5]

Para listas con exactamente 3 elementos, z equivaldría a [].


  • ¿Es mejor estilo de programación usar la regla de layout o llaves y puntos y coma como separador?

Puedes usar el que mas te convenga, personalmente creo que resulta un código mas entendible usando la regla de layout.


  • ¿En qué beneficia el uso de funciones lambda a las funciones de orden superior?

Si desea escribir una función de orden superior, deberá declarar uno o más argumentos para recibir delegados, y a menudo se utilizan expresiones lambda como argumentos de dichas funciones de orden superior.
La utilización de expresiones lambda es beneficio en las funciones de orden superior, ya que se puede definir directamente como argumento, lo que ahorra código, y ahorra la necesidad de definir la función con anterioridad.

  • ¿Cual es la diferencia entre la función factorial y afactorial? ¿Qué motivo tiene en este ejemplo el uso de patrones aritméticos?

La diferencia entre factorial y afactorial es que la primera si se llama con un parámetro negativo entra en un bucle infinito y la segunda daría un error, ya que ningún patrón concuerda con el parámetro pasado. No concuerda porque al poner el patrón como (n+1) sólo pasan los argumentos con valor mayores a 1. El motivo del patrón aritmético es precisamente este. Para quien no sepa cuales son estas funciones se muestran su implementación a continuación:

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial(n - 1)

afactorial :: Integer -> Integer
afactorial 0 = 1
afactorial (n + 1) = (n + 1) * afactorial n
  • ¿A qué nos referimos cuando se dice que para resolver el patrón hace falta evaluar un parámetro hasta obtener un
  • El patrón subrayado indica un buen estilo de programación pero ¿afecta a algo más el uso o no uso de este patrón?

Es una norma de estilo para indicar que no se va a utilizar y facilitar así la lectura al programador. Además nos evita la declaración de una variable que no va a ser usada en el programa.

  • En una expresión case, ¿no hay ninguna forma de utilizar patrones de distinto tipo?
  • Existe en Haskell alguna forma de crear en tiempo de ejecución nuevas funciones.
  • En los patrones nombrados o seudónimos, como por ejemplo m@(n+1), puesto que m se asocia con n + 1, si por algún motivo, se modifica el valor de n en la función, ¿m cambia de valor también?

Si modificamos m dentro de la función, m efectivamente se verá modificada, pero no así la n, que mantendrá el valor que se asignó antes de comenzar a evaluar la función.

Veámoslo con un ejemplo:

 myn :: Integer -> (Integer, Integer)
 myn m@(n+1) = (m,n)  where m = 2;

El resultado que obtenemos es:

 Hugs.Base> :load prueba.hs
 Main> myn 4
 (2,3)
 Main> myn 10
 (2,9)

Podemos apreciar como aunque cambiemos el valor de m, la variable n siempre contendrá el valor que inicialmente tenía m menos 1.

  • ¿Es recomendado el uso del patrón subrayado "_", para cualquier parámetro que no se vaya a emplear en la función? ¿O es indiferente?

Sí es recomendable, ya que es considerado como buen estilo de programación además de que va a evitar cometer errores tipográficos.

  • Existen dos posibles estilos de programación en Haskell, mediante sangrado o con llaves y el separador punto y coma. ¿Cuál es más recomendable y fácil de emplear?

La más recomendable de usar es mediante el sangrado puesto que de esta forma nos vemos obligado a que nuestro código quede bien sangrado y sea mucho mas facil de leer, tanto para nosotros mismo como para otros programadores que quieran leer nuestro código.

  • ¿Qué es mejor, una función definida a trozos o bien con patrones constantes, en la que se defina un patrón para cada caso?
  • ¿Se puede usar un patrón alias en el que aparezca más de un argumento de la definición de la función?

No, como se puede ver en el siguiente código que da error en el intérprete:

     fun :: Integer -> Integer -> Integer
     fun m@(x + y ) = m
  • ¿Que es mejor estilo de programación utilizar expresiones condicionales o las funciones a trozos?
  • En el capítulo se dice que en Haskell un nombre de variable no puede aparecer repetido en la parte izquierda de una ecuación, pero, en el caso de los seudónimos, ¿Se puede nombrar un patrón con el mismo nombre de la variable que aparece en dicho patrón?
  • ¿Hay alguna forma de hacer visibles los argumentos de una función fuera del ámbito de esta?
  • Entre una función que use patrones constantes y una función definida a trozos, ¿Cuál sería más conveniente?
  • ¿De qué manera suelen utilizarse las expresiones lambda al manejar funciones de orden superior?
  • Entre el sangrado y los corchetes con punto y coma, ¿Qué suele ser más recomendable y por qué?
  • Otra forma de escribir la función factorial sería de esta forma
  • Dentro de la definición de una función ¿sería posible utilizar usar una función anónima creada fuera de ella?
  • ¿Es posible definir funciones lambda recursivas?
Teoricamente sí, buscando información sobre combinadores me he encontrado con la respuesta. Para definir funciones lambda recursivas es necesario usar un Y-Combinator, como se sale del ámbito de este tema lo dejo a modo de comentario simplemente por si se quiere investigar sobre el tema.
  • ¿Qué significa que el patrón subrayado no establece ligadura alguna?

Cuando no utilizamos el patrón subrayado en la parte izquierda de la definición, el patrón utilizado se asociará con el valor correspondiente. En cambio, el patrón subrayado se unificará con cualquier argumento pero no establecerá la asociación anterior, es decir, no establecerá ligadura alguna. Veamos un ejemplo de lo expuesto:

longitud :: [Integer] -> Integer
longitud [] = 0
longitud (x:xs) = 1 + longitud xs

En el ejemplo anterior, se establecerá un asociación entre x y el valor del primer elemento de la lista que se pasa como argumento. En cambio, si utilizamos el patrón subrayado, ya que en la parte derecha de la definición no hace falta utilizar el valor de x, esta asociación o ligadura no se establecerá y, por tanto, podemos decir que el código será más eficiente.

longitud :: [Integer] -> Integer
longitud [] = 0
longitud (_:xs) = 1 + longitud xs
  • En esta expresión vacía :: [a] -> Bool ¿por qué se escribe a y no el tipo de parámetro que se le pasa?

Porque se le va a pasar una lista como argumento que puede ser una lista de cualquier tipo (Integer, Int, Char, Bool..) Como ejemplo podemos poner una función que mire si una lista está vacía sin importar de que tipo sean los elementos de la lista:

vacia::[a]->Bool
vacia [] = True
vacia (x:xs) = False
Main> vacia [True]
False
Main> vacia [4]
False
Main> vacia ['a']
False
Main> vacia[]
True
  • En los patrones constantes dice que pueden ser un número, un carácter o un constructor de datos. ¿A qué se refiere con esto último?
  • Haciendo la reducción de factorial 3 obtengo (2+1) * factorial * ¿La elección del redex más externo (2+1) o el más interno (factorial 2) para continuar es mía? ¿Una vez elegido un criterio hay que mantenerlo hasta el final?
  • ¿Es más óptimo definir una función utilizando patrones que por trozos?
  • Si necesitáramos en el cuerpo de una función hacer referencia al tercer elemento de una lista, pienso que el patrón correcto sería (_:_:x:_). ¿Es esto correcto?
  • ¿Una expresión lamba puede contener mas de una sentencia?

No, el cuerpo de una expresión lambda queda restringido a una única sentencia.

  • ¿Existe la posibilidad de definir una expresión lambda, como una función a trozos?
  • ¿Se pueden incluir declaraciones, o sólo admite definiciones de funciones, dentro de WHERE y LET ?
  • La función case, ¿qué me permite hacer que no pueda hacer directamente con los patrones sin hacer uso de dicha función?

¿ Se pueden aplicar patrones aritméticos a una lista ?

¿ Porque en el ejercicio 9 del Sumatorio al introducir un número negativo nos dá un desbordamiento de pila mientras que en el de lista infinita sigue escribiendo la lista de forma infinita?


  • Ejercicio 2.26 del Libro: Escriba una función descomponer:: Integer->(Integer,Integer, Integer) que a partir de una cantidad de segundos, devuelva una terna con las horas, minutos y segundos equivalentes.
descomponer :: Integer -> (Integer, Integer, Integer)
descomponer x 
        |x >= 0 = (h, m, s)
        |otherwise = error "Número negativo no válido"
         where 
                h = div x 3600
                m = div (mod x 3600) 60
                s = mod (mod x 3600) 60
  • ¿Existe la posibilidad dentro del intérprete Haskell de mostrar las reducciones que realiza para resolver una función? Si una especie de debug donde se muestre los pasos que sigue a la hora de reducir.
  • Ejercicio 2.14 del Libro: Defina una función aEntero :: [Integer] -> Integer que transforme una lista de dígitos en el correspondiente valor entero. ¿Puede realizarse este ejercicio sin utilizar la función invierte?
 aEntero :: [Integer] -> Integer
 aEntero [x] = x
 aEntero (x:y:z) = aEntero ((10*x+y):z)


  • ¿Qué denota el valor especial "T invertida" que devuelve la función error?
  • ¿Se pueden anidar IF en Haskell?

Efectivamente se pueden anidar los IF. Siempre aplicando las reglas de layout. Un ejemplo a continuación, que se podría realizar con funciones a trozos

  signo :: Integer -> Integer
  signo x = if x > 0 then 1 else
               if x == 0 then 0 else -1


Funciones de orden superior y polimorfismo

  • ¿Cual es la finalidad de las n-reducciones?

Finalidad práctica, ninguna. Es simplemente un medio de simplificar expresiones complejas cuando a estas le sobran parámetros.

  • Si las funciones no parcializadas no permiten tanta flexibilidad como las parcializadas, ¿porque se utilizan estas, si cualquier función no parcializada podemos parcializarla?.
  • ¿Es posible escribir cualquier función utilizando expresiones lambda?. En ese caso,¿ cómo se representan cuando un parametro de la función es otra función?

Sí es posible, de hecho, si atendemos a la teoría de la computabilidad, todo es una máquina de Turing. Es decir, que tanto el cálculo Lambda, como C++, Haskell o Java tienen las mismas capacidades computacionales (con ellos se pueden escribir los mismos programas). Las diferencias entre los lenguajes radican en las facilidades que dispongan para una u otra tarea.

En cálculo lambda, una función se recibe como cualquier otra variable:

((\lambda f. f\mbox{ }3) (\lambda x. x + 5)) \rightarrow 8

En este ejemplo, la función espera como parámetro una función a la que se le pasa como parámetro 3. La función enviada es x+5. Al pasarle el argumento 3, se sustituye x por 3 resultando 3+5 = 8.

  • ¿Existe alguna manera de comprobar si la operación que se va a aplicar sobre un parámetro de una función polimórfica es válida? Es decir, en una función polimórfica no se declara el tipo del parámetro, por lo que puede darse el caso que la operación que vamos a ejecutar no se pueda realizar y nos dé un error.

El sistema devolverá el siguiente error: Cannot infer instance, indicando que no es capaz de hacer que los tipos (el enviado y el esperado) coincidan. Es decir, no puede inferir la instancia común a ambos.

Por ejemplo:

Hugs>(\x->x+3) [2]
ERROR - Cannot infer instance
*** Instance    : Num [a]
*** Expression  : (\x->x+3) [2]

El operador '+' no está definido sobre listas, entonces no existe ninguna instancia de tipo aplicable a su vez a x, a 3, y a [2].

  • ¿La composición de funciones se puede usar con cualquier función, o sólo con los operadores unarios y con funciones que tengan un sólo parámetro? ¿Hay entonces que parcializar siempre las funciones antes de usar la composición?
  • No me queda claro el concepto de sección, o más bien la diferencia entre parcialización y la aplicación de una sección
  • ¿Cuál es la principal aplicación de las funciones no parcializadas?
  • ¿Cómo se puede hacer para que en una función parcializada, el parámetro que se le pase implícitamente sea el segundo y no sea el primero por defecto? Por ejemplo en una función de dos parámetros.

Esto se puede hacer con la función flip que viene pre-definida en Haskell.

    flip :: (a -> b -> c) -> (b -> a -> c)
    flip f x y = f y x

Esta función lo que hace es invertir el orden de los parámetros. Por ejemplo si tenemos la función:

    divide :: Float -> Float -> Float
    divide x y = x / y

y queremos parcializar dando por defecto el denominador, no podemos hacer:

    divide2 :: Float -> Float
    divide2 = divide 2

porque estaríamos proporcionando el primer parámetro de divide, es decir la x, por lo tanto tenemos que darle la vuelta a los parámetros con flip:

    divide2 :: Float -> Float
    divide2 = flip divide 2

y obtendremos el resultado deseado.

  • ¿Las \lambda-\mbox{expresiones} también se pueden expresar de forma no parcializada?
  • ¿Cómo se podría definir, por ejemplo, la función curry, de manera que acepte todo tipo de funciones con muchos parámetros?
  • ¿Las secciones sólo se pueden emplear dentro del cuerpo de una función?
  • ¿Hay alguna manera de usar las secciones de operadores sin paréntesis o siempre son obligatorios?

Son obligatorios, ya que hay que convertirlo a función. Es parecido a lo que ocurre cuando lo pasamo a una función de orden superior, que también debemos convertirlo en función.

  • ¿Se pueden parcializar las funciones lambda? No, porque para parcializar una función se trabaja sobre su nombre de función, y las lambda funciones no se guardan con un nombre. Son instancias de una función.
  • Los combinadores, ¿son una definición para una familia de funciones o son realmente una construcción del lenguaje?

Los combinadores son definiciones de funciones que nos sirven para agrupar un conjunto de funciones que actúan de forma similar, ahorrandonos así el tener que definir el cuerpo de cada función. Por tanto, son definiciones que el usuario crea para reflejar esquemas de cómputos comunes.

  • Se dice que las secciones son útiles cuando se trabaja con funciones de orden superior, creo que entiendo el concepto de orden superior: en el que una función puede recibir o devolver como parámetro otra función. Pero ¿qué afecta esto al uso de secciones? (no lo tengo nada claro).

Que una sección es, al fin y al cabo, una función. Por ejemplo, si tenemos la función

(+) x y = x + y

La sección (+2) sería tal que así:

(+) x = x + 2

De este modo, la sección (+2), por ser una sección, puede ser entregada a una función de orden superior como parámetro.

  • ¿Podríamos definir la función doble utilizando secciones de la siguiente manera?:
  • Entre las distintas sintaxis válidas para las expresiones lambda, ¿hay alguna que deba utilizarse por convenio?
  • ¿Por qué se diferencia entre secciones y parcialización? Aparte de actuar uno sobre operadores y el otro sobre funciones, ¿existe alguna otra diferencia?
  • ¿Por qué aparece esa filosofía de que una función que recibe dos parámetros es en realidad una función que recibe un parámetro y que devuelve otra función?

La currificación dice que "Si ajustas algunos argumentos, tendrás una función de los argumentos restantes". Por ejemplo, si la función div significa la versión currificada de la operación x / y, entonces div con el parámetro x ajustado en 1 es otra función: igual que la función inv que devuelve la inversa multiplicativa de sus argumentos, definida por inv(y) = 1 / y.

La motivación práctica para currificar es que en muchas ocasiones, las funciones que se obtienen al utilizar algunos argumentos en una función currificada pueden resultar útiles; por ejemplo, muchos lenguajes tienen una función o un operador similar a 'incrementar'. Currificar hace fácil definir dichas funciones.

  • ¿ Cuáles son las ventajas de las funciones parcializadas?

La parcialización permite un uso más versátil de las funciones ya que en una función de n argumentos es posible aplicarles desde o hasta n argumentos. En cambio en otros lenguajes de programación una funcion que recibe n argumentos recibe n argumentos exactamente. También facilita el trabajo con funciones de orden superior.

  • ¿Cuál sería la implementación de la función curry que recibe más de dos parámetros?
  • Buscando información por internet he encontrado este ejemplo:
  • En relación a las "secciones", ¿en qué se diferencian las siguientes funciones?
  • No he entendido el concepto de n-reducción. ¿Si cancelas parámetros de la función no pierdes funcionalidad?
  • ¿Es posible crear una sección para el operador resta? Entiendo que si la declaramos del tipo (-3) por ejemplo se trata del operador unario.

El operador (-) es una excepción de las secciones. Por ejemplo, la expresión (- e), no representa una sección, sino la negación de e. El operador - es la única función simbólica unaria en Haskell. El programador no puede definir nuevos operadores unarios, por eso, es la única excepción posible en las secciones.

  • ¿Podemos decir que la utilización de secciones está casi enfocado a ser usadas con funciones de orden superior?


  • ¿Se puede usar un combinador dentro de una función, para definir partes iguales dentro de ella misma?
  • ¿Es similar una variable de tipo en Haskell a una declaración de tipo object de otros lenguajes? En caso negativo, ¿cuál es la diferencia?
  • ¿Existe alguna librería de combinadores?
En el prelude de haskell se definen combinadores a partir de los cuales se pueden definir otras funciones, pero además existen librerías que también contienen :algunos, entre ellas:
  • StrategyLib: librería de combinadores (paquete Strafunski)
  • QuickCheck: Esta librería ofrece combinadores para definir propiedades, observar la distribución de los datos de prueba, y definir generadores de datos de prueba.
  • ¿Podemos determinar si un combinador es bueno o malo en función de la cantidad de funciones recursivas que se adapten al patrón de dicho combinador?
  • ¿Cuál es la finalidad de utilizar el operador $?

La finalidad del operador $ es evaluar una composición de funciones en un punto sin escribir ésta entre paréntesis, al tener menor prioridad que la composición. Su función es evitar que el uso de ésta composición tenga que estar entre paréntesis. Tenemos un ejemplo:

  PRELUDE> ((+1).(*3))10
  31 :: Integer
  PRELUDE> (+1).(*3) 10
  31 :: Integer

El resultado es el mismo, pero evitamos la introducción de los paréntesis en la composición a través del uso de $


  • Cuando utilizamos funciones polimorficas, debe determinarse el tipo de dato con el que se está trabajando. ¿ Es más eficiente utilizar una definición de tipos en una función que usar polimorfismo?, puesto que no es necesario determinar el tipo de los datos y en con polimorfismo si.
  • Las funciones sin declaración, evidentemente, tienen un comportamiento polimórfico, puesto que aceptan cualquier tipo ¿pero hay alguna diferencia por el hecho de no tener variables de tipo?
  • Cuando a un tipo genérico le aplicas un operador, existe una suposición de que dicha operación está definida para dicho tipo. ¿Hay alguna forma elegante de declarar dichas "suposiciones" para funciones paramétricas en Haskell?, ¿algún patrón?, ¿algo parecido a la idea de "concepto" o "axioma" de C++0x?
No,
Además saliéndonos un poco del tema, creo que los conceptos de C++0x responden más a la idea de interface de Java.
  • Si intento definir la función
aplica :: [a -> b] -> a -> [c]

el compilador de Haskell me devuelve el siguiente error:

Inferred type is not general enough
*** Expression : aplica
*** Expected type : [a -> b] -> a -> [c]
*** Inferred type : [a -> b] -> a -> [b]

¿Por qué?

Porque ya que "[c]" recolecta los resultados de aplicar el segundo parámetro de tipo a, en cada una de las funciones de [a*>b], esa lista "[c]", no debería ser "[c]", sino "[b]", para que concuerde con las "salidas" de las funciones. Si un conjunto de máquinas producen trigo, sus resultados deben guardarse en una bolsa de trigo, no de otra cosa.
Espero haberme explicado.
  • ¿Podríais poner un ejemplo de utilización de flip de uso común?

Un ejemplo de uso común podría ser:

Main> foldl (flip (:) ) []"Prueba ejemplo"
"olpmeje abeurP"

  • ¿Cuáles son todos los combinadores definidos en el prelude de Haskell a partir de los cuales pueden definirse la mayoría de funciones?
Podéis acceder a todas las funciones que están definidas en el prelude desde este enlace.
  • ¿Se puede escribir funciones utilizando combinadores, en los que el 'op' sea una expresión lambda?
  • Al hacer :t sobre la función "iter" se obtiene "Integral a => (a -> b -> b) ..." ¿Qué significa ese "Integral a =>"?
  • ¿Que utilidad tiene definir funciones mediante el estilo point free? ¿Podría indicar alguna definición de alguna función usando este estilo, y algún ejemplo de uso?
A veces se suelen escribir funciones con el estilo pointfree para hacerlas más compactas.
Con este estilo se obtienen (o por lo menos se persiguen) versiones más claras y cómodas de usar y entender.
Un ejemplo lo puedes ver aquí:
demopf x y = 2 * x + y
en estilo pointfree sería:
demopf = (+) . (2 *)
donde (2 *) sería la función que dobla al argumento, y (+) es la función de suma (currificada)


  • ¿Por qué el tipo más general de la función dosVeces f x = f x es dosVeces::(a->b)-> a->b?


Definiciones de tipos y sistema de clases

  • ¿Por qué es necesario poner los paréntesis en los constructores de datos Entero n y Letra c de las guardas, si no hay nada más a la derecha?

Ejemplo del libro, pág. 74:

incLoE :: LetraOEntero -> LetraOEntero
incLoE (Entero n) = Entero.(+1) $ n
incLoE (Letra c) = Letra.chr.(+1).ord $ c
Porque los constructores de datos pueden utilizarse para identificar patrones.
  • En el ejemplo del libro, donde declara el constructor de datos de números complejos :-, lo hace de la siguiente forma:
data Complejo = Float :- Float deriving Show

¿Es válido lo siguiente?

data Complejo = :- Float Float deriving Show
En todo caso sería:
data Complejo = (:-) Float Float deriving Show
  • A la hora de hacer una unión o un producto, ¿se puede usar como componente de un constructor de datos, un sinónimo de tipo de una función?
  • ¿Se pueden utilizar tipos enumerados, productos o uniones para volver a definir una nueva unión, producto, siempre y cuando estos se usen como componente de un constructor de datos?
Sí, valga como ejemplo:
data CaracterOEntero = Caracter Char | Entero Integer
deriving Show
data ParCE = Par { x :: CaracterOEntero, y :: CaracterOEntero } deriving Show
prueba = Par { x = Entero 2 , y = Caracter 'e'}
Main> prueba
Par {x = Entero 2, y = Caracter 'e'}
Main> x prueba
Entero 2
Main> y prueba
Caracter 'e'
  • ¿Para qué sirve el deriving Show y por qué se usa en cada constructor de tipo de dato?
La cláusula deriving show es necesaria para que HUGS pueda mostrar por pantalla los valores del tipo.
  • ¿Qué otros tipos de datos a partir de otros tipos existen en el Prelude?
  • ¿Cuántas funciones u operaciones se pueden aplicar con el término deriving?
  • En el ejemplo de la página 74 del libro "Razonando con Haskell":
Data Racional = Par Integer Integer deriving Show
  • ¿El término Par es un constructor definido en el Prelude?

No, el término Par es un constructor que en caso de querer usarlo, es el usuario el que tiene que definirlo, como por ejemplo de la siguiente manera:

data Racional = Par Integer Integer deriving Show 

En este ejemplo, el constructor Par tendría el siguiente tipo:

Par :: Integer -> Integer -> Racional 
  • ¿Qué es un constructor de datos simbólico?

Es un constructor en el cual no se utilizan letras, solamente símbolos. Deben empezar por el carácter ':'.

  • Al nombrar las componentes de un tipo se introducen automáticamente funciones selectoras para éstas, entonces: ¿tendríamos problemas si tenemos una función definida con el nombre de una componente de un tipo?
Tendríamos problemas si la función concuerda en el nombre del componente y en que recibe el producto. Me explico, en el caso de Persona: Tenemos la siguiente función:
Edad :: Integer -> Integer
No habría problemas, porque la función consultora que se cree para Persona tendrá la siguiente declaración:
Edad :: Persona -> Integer
Si tuvieramos una función definida con ese prototipo evidentemente sí.
  • Si Char e Integer son constructores de tipo, ¿cuáles son sus constructores de datos? 

Char e Integer son constructores de tipo carácter y de tipo entero, para los cuales sus constructores de datos serían por ejemplo 'a' y 8 respectivamente. Es decir, sus constructores de datos serían los propios valores de su contenido, 'b' es el constructor de datos del tipo Char.

  • ¿Pueden los elementos de un enum comenzar por una letra minúscula?

No, ya que al igual que un constructor de tipos, todo constructor de datos debe empezar siempre por una letra mayúscula.

Ejemplo:

data Dirección = Norte | Sur | Este | Oeste deriving Show
  • Qué significado tiene la siguiente afirmación:

se introducen automaticamente funciones selectoras para las componentes:

MAIN >: t edad
edad:: Persona -> Edad "
Que se crean funciones consultoras para cada componente del Producto. En el ejemplo de persona, una para edad, nombre, apellido1 y apellido2.
  • ¿Qué prioridad tienen los identificadores de constructores binarios de datos simbólicos?
  • A la hora de definir un tipo de dato, ¿se pueden hacer de forma incompleta, es decir, para un rango que no se definan? ¿Y definir un valor por defecto?
  • Cuando creamos un tipo de dato complejo usando uniones, ¿se limita de alguna forma el número a utilizar?
  • ¿Qué quiere decir que un mismo constructor de datos no puede aparecer en dos tipos distintos en un mismo ámbito?

Significa que si por ejemplo definimos un tipo semana asi:

   data DiaSemana = Lunes | Martes | Miercoles | Jueves | Viernes | Sabado | Domingo deriving Show. 

Luego no podremos definir otro en el cual se muestre alguno de los días que componen esta semana como por ejemplo sería:

   data FinSemana = Sabado | Domingo deriving Show.

Para este efecto, podríamos crear una función que dado un DiaSemana devolviera si es o no fin de semana:

   esFinSemana :: DiaSemana -> Bool
esFinSemana Sabado = True
esFinSemana Domingo = True
esFinSemana _ = False
  • ¿Qué diferencia existe entre un sinónimo de tipo y un tipo de datos?
A parte de la forma de declararlos (mediante la palabra reservada type/data).
  • ¿Por qué es necesario indicar un nombre para cada elemento en las uniones? ¿No basta con indicar el tipo? ¿Hay alguna forma de acceder a cada elemento por su nombre?

En principio el tipo unión define un nuevo tipo de datos a partir de varios existentes, aunque éstos se pueden crear justo en el momento de la creación de la unión. Usando un ejemplo del libro:

   data LetraOEntero = Letra Char | Entero Integer deriving Show 

se crea un tipo LetraOEntero a partir de Letra y Entero, por lo que el nombre de los tipos debe existir. En la unión no se crea un dato que tiene un elemento de cada uno de los tipos que la compone, se crea un dato con uno de los tipos que la compone, así por ejemplo:

   f1 :: LetraOEntero
f1 = Letra 'x'
f2 :: LetraOEntero
f2 = Integer 2

Si llamamos a f1, se creará una LetraOEntero a traves del constructor de Letra, y devolverá la LetraOEntero 'x' (Char). En el caso de llamanr a f2, se creará una LetraOEntero a traves del constructor de Integer, y devolverá la LetraOEntero 2 (Integer)

  • Los identificadores de constructores binarios de dato también pueden ser simbólicos, pero que en este caso siempre han de comenzar por el caracter ':'. ¿Qué limitación hay en los símbolos que pueden ponerse detrás del caracter dos puntos? ¿La elección de uno u otro es personal (por razones estéticas, por ejemplo)?
  • Utilidad del valor undefined
Es un valor que tienen todos los tipos para indicar que cierto valor o función no está definido en ese tipo. se puede usar este valor para capturar un error en una función y si se produce 'undefined' tratarlo como mejor nos sea necesario.
  • ¿Por qué se ponen los operadores entre ':', como por ejemplo ":+:"? Es más, ¿qué sentido tiene declarar una función que haga lo mismo que los distintos operadores?
Porque son identificadores simbólicos (indentificador formado por símbolos, no letras) de constructores de datos binarios, que obligatoriamente deben empezar por el caracter ":".

  • ¿Qué es una función de plegado? ¿Para qué sirve?
Una función de plegado engloba el comportamiento común de las funciones que recorren una lista para calcular un resultado. La idea es similar al combinador iter.
  • Qué diferencia hay entre usar data y usar newtype? Un ejemplo en el que se use data y otro en el que se use newtype.


  • ¿Por qué razón todas las clases en Haskell son uniparamétricas? ¿Existe alguna posibilidad de definir una que sea multiparamétrica?
Las clases de Haskell son monarias debido a que aglutinan una serie de operaciones sobre un Tipo. Es dificil conceptualmente pensar en una clase con más de un parámetro. Despues de todo bastaria con definir un nuevo tipo y ya seria uniparametrica. Aún así probamos con un ejemplo tonto:
class Bi a b where
f :: a -> b -> Bool
f a b = (a,b)

instance Bi Int Int
al cargar el archivo en el interprete hugs. Nos sale el siguiente error
ERROR "multiparametria.hs":1 - Haskell 98 does not support multiple parameter classes.
Así que no es posible.

  • ¿A qué nos referimos con instancias automáticas?
A instancias que se han creado utilizando la cláusula deriving y no han sido programadas explícitamente por el programador.
Si definimos
data Par = Par Float Float deriving ( Show,Eq)
al derivarlo de Eq hemos hecho una instancia automatica de esa clase, lo que nos genera (==) y (/=) para este nuevo tipo de dato. Puede comprobarse en el intérprete
Main> (Par 2 3) == (Par 2 3)
True


Listas, árboles y grafos

 
  • ¿Qué se quiere decir cuando al hablar de los cualificadores se dice que "Si se introduce más de un generador, los que aparecen a la derecha cambian más rápido" ?

En la sintaxis de las listas por comprensión, efectivamente puede aparecer más de un cualificador, y esos cualificadores entre otra cosas pueden ser "generadores de lista". Pues si existen dos generadores de lista, es importante en el orden en que aparezcan; ya que la posición indicará el orden en que se combinen, formando así la lista resultado. Vamos a verlo mejor con un ejemplo:

Hugs> [(x,y)|x<-[1..3], y<-['a','b']] 
[(1,'a'),(1,'b'),(2,'a'),(2,'b'),(3,'a'),(3,'b')] 

En este ejemplo, formaremos pares en los que siempre los "números" se encontraran en la primera posición, y las "letras" en la segunda posición del par formado. Pero en este caso concreto al estar el generador y más a la derecha, las letras serán las primeras en cambiar, quedando fijo el primer número "1"; cuando se terminen las letras, entonces cambiarán los números dando lugar al segundo número que es el "2".....y así hasta completar la lista resultado.

Si hubiésemos puesto el generador x más a la derecha, de tal forma:

Hugs> [(x,y)|y<-['a','b'], x<-[1..3]] 
[(1,'a'),(2,'a'),(3,'a'),(1,'b'),(2,'b'),(3,'b')] 
  

en la lista resultado, los pares formados son los mismos, pero el orden cambiaría, cambiando más rapidamente los números, que son los generados por x.


  • ¿Podría decirse que las funciones de plegado son o funcionan como los combinadores?
  • Estas dos expresiones son equivalentes. Mi pregunta es, ¿cual de las dos es más eficiente?
  • En el capítulo se dice que la principal ventaja de las listas por comprensión es la claridad, ¿Qué otras ventajas presentan?
  • Se dice que es posible escribir cualquier expresión sin utilizar la sintaxis de listas por comprensión ¿Hay alguna excepción a esto ?

No hay ninguna excepción a esto. Es posible gracias a las existencias de unas reglas llamadas semántica de listas por compresión que permite pasar una lista de este tipo a una expresión sin estas.

  • ¿Se podría definir un tipo basado en el tipo lista en el que la recursión se hiciese por la izquierda en vez de por la derecha, algo así como
  • En las listas por comprensión, no me esperaba el comportamiento que tienen cuando se usa más de un generador y en la expresión solamente se usa uno de ellos:
  • En un programa en Haskell, ¿Varía mucho su eficiencia con el uso de las funciones tipo foldr, foldl...? Hago esta pregunta porque, por ejemplo, en una de las transparencias del tema aparece un ejemplo de uso de la función foldr que es el siguiente:
  • Por otro lado, he observado que, en general, las funciones que podemos ver durante este tema necesitan listas que contengan elementos del mismo tipo siempre. Hemos visto ya que Haskell puede crear tipos como son las uniones o los enumerados. ¿No conseguiría Haskell ser más potente, si, por ejemplo, a partir de una lista que tuviera caracteres y enteros creara directamente un nuevo tipo como el que vimos de CaracterOEntero?. Me refiero a hacerlo de forma transparente al usuario.
  • Si no se cumple una precondición, ¿Existe alguna forma de indicarlo al usuario y abortar la ejecución manualmente?
  • En cuanto a las listas se refiere, ¿es indiferente la concatenación de estas utilizando (++) o concat?

Existe una pequeña diferencia, en el "uso" o forma de operar, pero no en cuanto a su "funcionalidad" que es la misma. Es decir con la función "concat" includia en el prelude, podemos concatenar muy cómodamente una "lista de listas", como en el siguiente ejemplo:

Hugs> concat [[1,2],[3,4],[5,6,7,8]]
[1,2,3,4,5,6,7,8]

Si quisiéramos hacer los mismo con el operador "++" , tendríamos que poner dos veces el operador, y por tanto no sería muy cómodo:

Hugs> [1,2]++[3,4]++[5,6,7,8]
[1,2,3,4,5,6,7,8]

De hecho la propia función "concat", está construida con el operador "++", de ahí la similitud:

concat       :: [ [a] ] -> [a]
concat []     = []
concat (x:xs) = x ++ concat xs


  • ¿Existe algún operador o método para concatenar listas de distinto tipo?

En Haskell una lista se define de modo que si a es un tipo cualquiera, [a] representa el tipo de listas cuyos elementos son valores de tipo a, luego todos los elementos de la lista deben ser del mismo tipo. Por ejemplo, [1, False, 'a'] no es una expresión válida en Haskell, por eso no se pueden concatenar listas de elementos de distintos tipos.


  • Entre la ordenación por inserción, por mezcla y rápida. ¿Cuál suele dar mejores resultados en Haskell en cuanto a eficiencia?
  • Practicando con las listas por comprensión probé lo siguiente:
  • El operador !! devuelve el elemento que se encuentra en la posición que le pasemos ([2, 4, 6, 8] !! 3 nos devuelve 8). ¿Existe algun operador o función para que nos devuelva en qué posición se encuentra un elemento?

Sí, en haskell podemos hacer uso de la función elemIndex definida dentro de Data.List para obtener la posición de dicho elemento.

Por ejemplo,

import Data.List 
elemIndex 'b' "abc" === Just 1 

 

                 Vemos como nos devuelve la posición del elemento b.

  • En Haskell se pueden implementar los algoritmos de ordenación ¿no existe un algoritmo implementado de este tipo para listas en los módulos de Haskell?
  • Definir, usando foldr,una función suma:: [Int]->Int tal que suma[x1, . . . , xn] = x1-x2+ x3-x4+ . . . + (-1)*n*xn 
suma lis = foldr (+) 0 (zipWith (*) lis aux)
           where aux = 1: aux1
                 aux1'= (-1):aux
<includeonly>{{Fin|5.1}}</includeonly>
  • Ya que no se ha realizado su corrección en clase, ¿cómo se resolvería el siguiente ejercicio? Realice la evaluación impaciente y la perezosa de la expresión suma 2 (desde 1) donde:
                  desde :: Integer -> [Integer]
                  desde x = x : desde (x + 1) suma :: Integer -> [Integer] -> Integer
                  suma n (x : xs) =
                       if n == 0 then 0
                                 else x + suma (n - 1) xs


  • No entiendo la declaración de fmap:
class Functor f where
fmap :: (a -> b) -> f a -> f b

¿que siginifica el segundo parámetro y lo que devuelve? ¿qué se le aplica a ese parámetro una función f y se devuelve otro al que también se le aplica una misma función f? ¿qué función f?


f no es una función, sino un constructor de tipos, así que f a, siendo a una variable de tipo, también será un tipo. Como no sabes cómo es la estructura de dicho tipo, ya que es arbitrario, tienes que delimitar de alguna manera que el tipo de los parámetros a los que vas a aplicar fmap sea el mismo, por eso, se especifica f a y f b.

f, en el caso visto en clase de la función fmap para el tipo Árbol, sería un Árbol, es decir, al aplicar una función con prototipo (a->b) al Árbol de elementos de tipo a (f a) se obtiene un Árbol de cuyos elementos serán de tipo b (f b). Por tanto, fmap se puede implementar para cualquier tipo polimórfico de aridad uno, ya sea un Árbol a o, por ejemplo, los tipos Pila a, Cola a, etc


  • ¿Cual sería una buena implementación de grafos NO dirigidos?
  • ¿Otra posible implementación de Grafo podría ser simplemente mediante lista de listas?
data Grafo v = G V deriving Show
g = G [[2,3],[4],[4],[]]
  • En la declaración del árbol binario
data ArbolBinario a = VacioBi | NodoBi a (ArbolBinario a) (ArbolBinario a) deriving show

¿cómo se agrupa tanto el nodo a como las dos ramas declaradas como ArbolBinario? Es decir, ¿es una estructura? A mi modo de ver son tres elementos separados sin ninguna unión de por medio.

  • Además de la implementación ya vista del árbol binario en Haskell, se podría implementar otro tipo de árboles tales como, árboles generales o árboles parcialmente ordenados?
  • se presentan las siguientes implementaciones para recorrer un árbol en orden:
enOrden :: ÁrbolB a -> [a]
enOrden VacíoB = []
enOrden (NodoB i r d) = enOrden i ++ (r : enOrden d)
enOrden' :: ÁrbolB a -> [a]
enOrden' x= aux x []
where
aux :: ÁrbolB a -> [a] -> [a]
aux VacíoB xs = xs
aux (NodoB i r d) xs = aux i (r : aux d xs)

Y se afirma que la segunda es más eficiente al utilizar un parámetro acumulador. ¿De qué manera se mejora la eficiencia?

  • ¿Existe en Haskell algún algoritmo implementado para trabajar con grafos (Dijkstra, Floyd, etc.)?

En este caso para Dijkstra, independientemente de la función de coste elegida, uno de los tipos de la función podría ser: dijkstra :: (p -> p -> c) -> p -> [p] -> [(p,c)] así la llamada podría ser por ejemplo: dijkstra func origen puntos

la llamada calcularía los costes de viajar desde el origen a todos los puntos de la lista puntos usando la función de coste func y devolviendo el resultado como una lista de pares (punto, coste) , [(p,c)] .


  • ¿Cómo podría definirse un árbol que contiene datos de dos tipos distintos?

data Árbol a b = Vacio | NodoA a [Árbol a b] | NodoB b [Árbol a b] deriving Show

Con esa definición de árbol ya podemos crearnos árboles de dos tipos diferentes:

a1 = NodoA 10 [a11, a12, a13]
where a11 = NodoB 'a' [NodoA 15 [Vacio], NodoB 'd' [Vacio]]
     a12 = NodoA 35 [Vacio]
     a13 = NodoB 'b' [NodoB 'c' [Vacio]]

  • ¿Existe alguna forma de restringir la definición de ÁrbolBinario en su definición que no sea textualmente?

En concreto restringir que los nodos que no tengan descendientes no pueden ser representados con:

NodoBi a VacíoBi VacíoBi

sino con:

Hoja a
  • Se dice que existe en Haskell un tipo de array predefinido que permite acceder y modificar una posición del array en tiempo constante. ¿ Qué implementación emplea este tipo?
  • En la implementación con listas por comprensión de fmap para el tipo Árbol, ¿por qué es incorrecta la siguiente implementación?
instance Functor Arbol where 
 fmap f Vacio = Vacio
 fmap f (Nodo a r) = Nodo (f a) [x | x <- fmap f r ]

en lugar de

[fmap f x | x <- r] 

Porque el x a la izquierda del símbolo '|' representa cada elemento de la lista, mientras que a su derecha, representa la lista entera, por ello, al compilar produce el siguiente error:

ERROR "Pruebas.hs":246 - Type error in application
*** Expression     : fmap f r                     
*** Term           : f                            
*** Type           : a -> b                       
*** Does not match : Arbol a -> b                 
*** Because        : unification would give infinite type

ya que al f debe recibir un tipo a, no un árbol de elementos de tipo a, que es lo que está recibiendo al pasarle la lista entera (que en este caso es la lista de nodos que se está procesando). Sin embargo, al ponerlo a la izquierda la función f se aplica a cada elemento de un nodo, que sí es de tipo a.

  • ¿ Existen en Haskell tablas hash?  
  • Cuando estamos recorriendo un grafo, ¿ Cómo puedo marcar aquellos nodos que he visitado y los que no? ¿ Puedo emplear alguna estructura nativa o que incorpore Haskell?
  • ¿En las listas pueden aparecer expresiones, que tengan el mismo tipo del resto de los elementos de la lista? Por ejemplo:[13,2 2,5 * x]
  • ¿Se podría implementar en Haskell el algoritmo de Kruskall, si es así, como sería su implementación?
  • No entiendo la utilización de la función breakque es una función predefinida para listas.
  • Como no hemos corregido el entregable 5.5, me queda esta duda: Justifique mediante la semántica de las listas por comprensión el resultado de las siguientes expresiones.
 [ x | y <- [1..5], x <- [5..10], even y]
 [ x | x <- [5..10], y <- [1..5], even y]

¿Alguien me la podría solucionar?


Evaluación perezosa y mónadas

  • ¿Una expresión do puede contener acciones de distinto tipo?

La notación do consiste en una secuencia de una o más acciones o ligaduras al resultado de una acción, efectuándose cada una de las acciones en el orden especificado. El tipo de una expresión do es el tipo de su última acción, pero el resto de acciones pueden ser de distinto tipo (sólo teniendo en cuenta la consideración de que el tipo de la expresión será el del tipo de su última acción). Por lo tanto, la expresión do SÍ puede contener acciones de tipos distintos. Véase el ejemplo que escribe la cadena introducida en mayúsculas:

   miAccion :: IO ()
  miAccion = do putStr "Dame un texto:"
                xs <- getLine
                putStr "En mayúsculas es:"
                putStr (map toUpper xs)

Vemos que la expresión xs -> getLine es del tipo a -> IO String, pero la acción putStr (map toUpper xs) es del tipo String -> IO (), respetando el tipo de la función miAccion.


  • ¿Es posible que se produzca un tipo de excepción que no pueda ser capturada por IOError?
  • ¿Cómo se podría modelar la característica de indeterminismo con mónadas?
  • La función return, encapsula un dato de tipo a, y lo convierte en una mónada del mismo tipo (M a), ¿como indicamos a return, cual es el tipo de mónada en la que encapsula dicho valor?
  • ¿La última acción de una lista de acciones do, debe ser del mismo tipo que el valor de la función donde se declara el bloque do?
   El tipo del bloque do viene dado por el valor (tipo) de la
  última expresión, luego el tipo de la salida de la función
  si debe ser del mismo tipo que el bloque do. Veamos un ejemplo
   -- Función que compara dos cadenas introducidas por teclado
  -- Entrada: dos cadenas desde teclado
  -- Salida: Sting 
   compara :: IO()
  compara = do 
         s1 <- getLine
         s2 <- getLine
         if s1 == s2 then putStr "Son iguales"
                     else putStr "Son diferentes"    


   La salida del bloque do es un String definido en el parámetro
  de salida de la función   
  • Si fmap es una generalización de map, ¿el método map es entonces un método de una subclase de la clase Functor?

No, map es una función perteneciente al módulo del prelude y la clase Functor es una clase definida también en el propio Prelude pero independiente, que define una única operación: fmap. La función fmap aplica una operación a los objetos pertenecientes a un contenedor (los tipos polimórficos pueden ser considerados como contenedores de valores de otro tipo), devolviendo un contenedor con la misma forma.

  • Qué funcionalidad tienen las siguientes funciones del módulo IO?:

- isIllegalOperation - isAlreadyExistsError - isDoesNotExistError - isAlreadyInUseError - isFullError - isEOFError - isPermissionError - isUserError

  • Dentro de la clase Monad, aparece la función fail. ¿Cuál es su cometido?
De acuerdo a la documentación oficial de Haskell.org la función fail con declaración:
fail :: String -> m a
[1,2,3] >>= (\ x -> [1,2,3] >>= (\y -> return (x/=y) >>=
Donde m se refiere a la clase Monad:
class Monad m where

....
Sirve para abortar mostrando un mensaje de error. Esta operación no cumple con la definición matemática de mónada, pero se invoca cuando se produce un fallo de pattern-match (encontrar el patrón) en una expresión do.
Viendo su definición a lo mejor se comprenda mejor su cometido:
fail s = error s
  • No llego a entender del todo bien el tipo del operador (>>=) y (>>) ¿El operador (>>) devuelve una mónada completa (es decir, no devuelve el valor sino la mónada en su totalidad)?
Podemos intuir la diferencia entre estos operadores por su declaración en la clase Monad:
class Monad m where
   ...
   (>>=) :: m a -> (a -> m b) -> m b
   (>>) :: m a -> m b -> m b
A partir de dicha declaración podemos intuir que el primer operador, >>=, espera el resultado de la primera función monádica para utilizarlo en la segunda. En cambio, >>, obvia la primera función, devolviendo siempre la segunda.
El uso del operador >>= es casi inmediato, ya que siempre es interesante utilizar un resultado de una función monádica anterior para utilizarlo en otra; pero quizás no se puede establecer uno tan claro para >>. Uno de los usos de este último operador podría ser la concatenación de varias funciones monádicas de salida.
Expliquémoslo mediante un ejemplo:
  • >>= espera el resultado de la función anterior y se lo pasa a la segunda función.
espera:: IO ()
espera = getLine >>= f
   where f x = print ("Salida: "++x)
Si escribimos en el intérprete:
 Hugs> espera
E introducimos una cadena cualquiera, por ejemplo: "Prueba de >>=", obtenemos el siguiente resultado:
Hugs> espera
Prueba de >>=

"Salida: Prueba de >>="

:: IO ()

Es decir, lo que ha recibido de la función getLine, f lo toma como parámetro.
  • >> No espera al resultado de la función anterior. Uno de los usos de este operador podría ser la concatenación de varias funciones monádicas de salida.
Por ejemplo, concatenemos las funciones anteriores con este operador, seguido de otra operación monádica de salida (otro print).
noEspera:: IO ()
noEspera = getLine >>= f >> print "Fin"
   where f x = print ("Salida: "++x)
De nuevo, introducimos otra cadena de prueba y observamos el resultado:
Hugs> noEspera
Prueba de >>

"Salida: Prueba de >>"

"Fin"
:: IO ()

A modo de aclaración la función print realiza lo mismo que putStr pero añade un salto de línea al final, de esta forma la salida se hace más legible.
  • ¿Se pueden anidar expresiones do? ¿Cuál sería el tipo de la función, el del bloque superior o el primer return que se ejecute?
Cada do inicia una secuencia de sentencias. Cualquier otra construcción que forme parte de la función, debe usar un nuevo do para iniciar otras secuencias de acciones.
Veamos un ejemplo:
getLine     :: IO String
getLine     =  do c <- getChar
                if c == '\n'
                     then return ""
                     else do l <- getLine
                             return (c:l)


  • ¿Es do la única forma de construir acciones a partir de otras?
Las acciones son combinadas secuencialmente usando el operador >>= (o "bind"). En vez de usar este operador directamente, usaremos una sintáxis más clara, la notación do, para ocultar el uso de los operadores de secuenciación gracias al uso de una sintaxis que recuerda a la de los lenguajes imperativos convencionales. La notación do puede ser traducida, de un modo trivial, a funciones Haskell normales, por ejemplo, las siguientes tres expresiones corresponden a diferentes modos de escribir lo mismo:
[1,2,3] >>= (\ x -> [1,2,3] >>= (\y -> return (x/=y) >>= 
(\r -> case r of True -> return (x,y)
_ -> fail "")))
[(x,y) | x <- [1,2,3] , y <- [1,2,3], x /= y]
do x <- [1,2,3]
  y <- [1,2,3]
  True <- return (x /= y)
  return (x,y)
 


  • ¿Posee algún método más que fmap la clase Functor?
Sólo posee el método fmap, siempre que el usuario no defina nuevos métodos para dicha clase
En el prelude se define la clase Functor como [2]:
 class Functor f where
  fmap :: (a -> b) -> f a -> f b
  • return convierte un valor en una acción. ¿Cómo se podría traducir "return ()" , por ejemplo? ¿La acción que cuando se realice devolverá como resultado ...?
return () devuelve una mónada vacía. Es igual que cuando ponemos en la declaración de una función, que el valor que devuelve es IO (). Los paréntesis () representan la cadena vacía.
  • ¿'<-' sólo puede almacenar el resultado de una acción o también se le puede asignar el valor de un tipo normal (por ejemplo, Integer)?
  • ¿Si hacemos que una función devuelva una mónada de un tipo concreto, como podemos luego captar esa mónada para ver el resultado?
Por ejemplo:
func :: IO String
func = do putStr "Escribe algo: "
         xs <- getLine return xs
Para poder mostrar el "contenido" de la mónada que devuelve la función que has definido, se podría usar, por ejemplo, el operador >>= y la función putStr:
Main> func >>= putStr
El prototipo de >>= es: Main> :t (>>=) (>>=) :: Monad a => a b -> (b -> a c) -> a c
y el de putStr: Main> :t putStr putStr :: String -> IO ()
Entonces, el prototipo de >>= para tu ejemplo concreto sería: IO String -> (String -> IO ()) -> IO ()
Es decir, >>= te va a transformar un dato de tipo IO String en otro de tipo IO() a partir de la función putStr (que transforma datos de tipo String en datos de tipo IO()). Esta descripción del operador >>= es la dada en la página 265 del libro "Razonando con Haskell" pero para los tipos concretos del ejemplo.
  • He estado probando el ejemplo "lee" de las transparencias y veo que si volvemos a llamar a la función lee tiene en cuenta lo que hayamos escrito en llamadas anteriores si la cadena es más larga que el número de caracteres que queremos leer:
  • ¿Existe alguna buena herramienta para encontrar/localizar funciones en haskell y sus prototipos?
    Sí. Existe Hoogle, en wikihaskell.org. La verdad es que es bastante útil. Es un buscador en el cual puedes introducir la funcion y te llevará hasta la página de documentación de dicha función, mediante la cual podremos ver el prototipo, así como algunos ejemplos de funcionamiento.

Herramientas personales