Biblioteca IO

De Wikihaskell
Saltar a: navegación, buscar
Entrada y salida
Funcionamiento y uso de flujos de entrada y salida
Lenguaje Haskell
Biblioteca IO
Autores Aris Burgos
Álvaro Galván
Silvia Garbarino

Bulat Ziganshin desarrolló una nueva biblioteca I/O en 2006 de la cual está muy orgulloso porque puede, en algunos casos, reemplazar a los servicios actuales de I/O basados en el uso de manejadores. La principal ventaja de esta nueva biblioteca es su fuerte diseño modular mediante el uso de tipos de clases.

La biblioteca consiste en pequeños módulos independientes, cada uno implementando un tipo de flujo (fichero, buffer de memoria, tuberías) o una parte de la funcionalidad común de los flujos (almacenamiento en buffer, codificación de caracteres). Bibliotecas de terceros pueden fácilmente añadir nuevos tipos de flujos y nuevas funcionalidad comunes.

Otros beneficios de esta nueva biblioteca se encuentra en el funcionamiento de los flujos como mónadas, compatibilidad Hugs y GHC, alta velocidad y la cómoda traslación de rutas de la existente biblioteca I/O.

Contenido

Introducción

El sistema de entrada/salida (E/S) de Haskell es funcional puro, y además, proporciona toda la expresividad que presentan los lenguajes de programación imperativos convencionales, manteniendo la transparencia referencial y sin efectos laterales.

En los lenguajes imperativos, los programas están formados por acciones que examinan y modifican el estado actual del sistema. Algunas acciones típicas son la lectura y modificación de variables globales, la escritura en ficheros, la lectura de datos y el manejo de ventanas en entornos gráficos. Haskell permite utilizar acciones de este tipo, aunque están claramente separadas del núcleo puramente funcional del lenguaje.

El sistema de E/S de Haskell está basado en un fundamento matemático que puede asustar a primera vista: las mónadas. Sin embargo, no es necesario conocer la teoría de mónadas subyacente para programar usando el sistema de E/S. Más bien, las mónadas son simplemente una estructura conceptual en las que las E/S encaja. No es necesario conocer la teoría de mónadas para trabajar con operaciones de E/S en Haskell del mismo modo que no es necesario conocer la teoría de grupos para trabajar con operaciones aritméticas simples. Una explicación en profundidad de las mónadas puede encontrarse en la sección Mónadas.


Motivación

La biblioteca GHC I/O (basada en el uso de manejadores (Handles)) tiene una característica muy rica, pero no puede ser ampliada nunca más. Esto es debido a su diseño no modular, donde existe un alto acoplamiento entre cada una de las características. Resulta indiscutible la necesidad de ampliarla, añadiendo las siguientes servicios:

  • Más modelos de Entrada/Salida asíncrona (soporte para kqueue, epoll, AIO)
  • Nombre de ficheros unicode en windows y unix
  • El empleo de ByteString/UTF8String/UTF16String (Cadenas de Bytes/Cadenas UTF8/Cadenas UTF16) para los nombres de ficheros.
  • Varias codificaciones (UTF8,UTF16...) para los ficheros de texto
  • Ficheros mayores de 4GB en windows
  • Ficheros de memoria mapeada
  • Cadenas de Bytes de E/S
  • E/S y serialización binaria


Propósito

Prerrequisitos: implementación en FPS o alguna otra biblioteca las operaciones comunes sobre ByteString/UTF8String/UTF16String y proporcionar alguna clase Stringable (que se pueda manejar como cadena) que proporcione una interfaz de tipos independiente a esas operaciones:

class Stringable a where
  length :: a -> Int
  concat :: [a] -> a
  ....

instance Stringable String
instance Stringable ByteString
instance Stringable UTF8String
instance Stringable UTF16String

La biblioteca debería incluir:

  • Módulos System.FilePath como el desarrollado por Neil Mitchell. Cambiado para funcionar con las operaciones de Stringable
  • Módulos System.Directory de Base. Cambiados para funcionar con Stringable y soportar los nombres de fichero Unicode (en Win32, lo que significa que hay que usar un conjunto de funciones en NT y otro en Win9x)
  • Módulo System.File de Streams/SSC, extendido para soportar Stringable, nombres de ficheros Unicode y ficheros mayores de 4GB para Win32
  • System.MappedFile para Streams/SSC, ditto


Mónadas

Las mónadas fueron introducidas por Eugenio Moggi en 1991 para estructurar las especificaciones denotacionales; más tarde, Wadler mostró que su uso se adapta a la descripción semántica de ciertas características de los lenguajes de programación, en particular usando lenguajes funcionales. Algunas de las características que se pueden modelar con mónadas son: indeterminismo, excepciones, estados, concurrencias, etc.

Las mónadas introducen un nuevo estilo de programación funcional, conocido como programación monádica. Los programas monádicos proporcionan una manera simple y atractiva de lograr la interacción en un entorno funcional. Es más, proporcionan un mecanismo de estructuración para tratar una amplia variedad de problemas, como por ejemplo

  • Tratamiento de excepciones
  • Análisis sintáctico
  • Computaciones basadas en estados

De hecho, como resultado del empleo de mónadas, obtenemos programas muy similares a los iterativos, como programas implementados en Pascal y C.


Mónadas IO

Comenzaremos considerando una mónada particular, la mónada IO \alpha\, de entrada-salida es para expresar programas que implican interacción con el entorno.

Esta es sin duda la más significativa pues ha sido la puerta de entrada de las mónadas al mundo de la programación. Su llegada ha traído luz a un sector oscuro de la programación funcional.

Los lenguajes funcionales puros son incompatibles con la Entrada/Salida debido a su opacidad referencial. Esta incompatibilidad se resuelve confinando los cómputos que realicen I/O dentro de la mónada. Al no proveerse mecanismos que permitan remover los datos contenidos por ella, se dice que es una mónada sin salida: los efectos colaterales quedan aislados del resto funcional puro del programa. Las acciones se van ejecutando a medida que se van ligando, permitiendo secuenciar una serie de cómputos en un estilo que emula muy bien al de la programación imperativa.

En Haskell, la función con la cual se dispara la ejecución, main, debe tener el tipo IO (). Es por esto que los programas en Haskell, son típicamente estructurados como una secuencia de acciones I/O de estilo imperativo en el nivel más alto, con llamadas al código funcional puro. Las funciones del módulo IO no realizan tareas de I/O por su cuenta, pero devuelven acciones I/O, que describen la operación deseada. Estas acciones pueden ser combinadas dentro de la mónada IO (de una manera funcional pura) para crear acciones I/O más complejas, resultando en la acción I/O final que es el valor principal del programa.


Ejemplo

A continuación, se mostrará un pequeño programa que pide al usuario que ingrese un texto, para luego invertirlo y mostrarlo.

Este programa responde a un esquema muy simple de entrada-procesamiento-salida.

El objetivo es mostrar como, efectivamente, la función main es el resultado de la combinación de acciones que, sin salir de la mónada IO, interactúan con el exterior para obtener y entregar datos, y con el interior funcional puro para procesarlos:

escribirLinea :: [Char] -> IO ()
escribirLinea [] = (putChar ’\n’) >> return ()
escribirLinea (c:cs) = (putChar c) >> (escribirLinea cs)
leerLinea :: IO [Char]
leerLinea = getChar >>= \c ->
   if c == ’\n’ then
      return []
   else
      leerLinea >>= \cs ->
      return (c : cs)
main :: IO ()
main = escribirLinea "Ingrese el texto: " >>
       leerLinea >>= \s ->
       escribirLinea (reverse s)

El Prelude de Haskell provee funciones para leer y escribir líneas pero, por cuestiones didácticas, se optó por crear unas nuevas con implementaciones bien explícitas, para enriquecer el ejemplo.

Es notable el aspecto imperativo de la función main. La función reverse forma parte del Prelude y juega el rol de código funcional puro encargado de procesar los datos.


Interacción monádica

El tipo de las órdenes de entrada-salida en Haskell se denota como IO( ). Este tipo es abstracto, no se nos dice cómo se representan los valores del tipo; lo importante son las operaciones que proporciona.

Una expresión de tipo IO( ) denota una acción; cuando se evalúa, la acción se realiza.

Lo interesante del tipo IO () son las operaciones que proporciona. Mas concretamente, en las operaciones de un tipo más general \mbox{IO }\alpha\,.

  • Una Operación fundamental es una función que imprime un carácter:
putChar :: Char -> IO()

Cuando evaluamos putChar 'a' se imprime el carácter a:

Main> putChar 'a'
a

Esta operación putChar la estudiaremos más adelante en la sección de Funciones básicas de Entrada/Salida predefinidas.


La mónada de entrada-salida IO \alpha\, es un tipo abstracto para el cual tenemos como mínimo, las siguientes operaciones:

  • \mbox{return :: }\alpha\, \rightarrow\mbox{ IO }\alpha\,
  • (>>=)\mbox{ :: IO }\alpha\, \rightarrow (\alpha\, \rightarrow\mbox{ IO }\beta\,) \rightarrow\mbox{ IO }\beta\,
  • \mbox{putChar :: Char }\rightarrow\mbox{ IO( )}
  • \mbox{getChar :: IO Char}\,


Las dos primeras operaciones son combinadores que caracterizan la clase de tipos denominada "mónadas". Por definición , M es una mónada si es una clase de tipos que posee las dos operaciones siguientes:


\mbox{class Monad M where}\,
\mbox{return :: }\alpha\,\rightarrow\mbox{ M }\alpha\,
(>>=)\mbox{ :: M }\alpha\,\rightarrow (\alpha\, \rightarrow\mbox{ M }\beta\,) \rightarrow\mbox{ M }\beta\,


El operador (>>=)\,, llamado también 'bind', permite construir una función que transforme datos de tipo \mbox{M }\alpha\, en datos de tipo \mbox{M }\beta\, a partir de una función que transforme datos de tipo \alpha\, en datos de tipo \mbox{M }\beta\,.

Figura: Definicion de (>>=)


La función return convierte un valor simple en el equivalente monádico.


Se requiere que estas dos operaciones satisfagan las siguientes leyes:

  • return es un elemento neutro por la derecha de >>
\mbox{p }>>=\mbox{ return }== p\,
  • return es un elemento neutro por la izquierda de >> en el sentido
(\mbox{return }e) >>= q = q e\,
  • >>= es asociativa en el siguiente sentido
(p >>= q) >>= r = p >>= s\mbox{ where }s x = (q x >>= r)\,

Las restantes operaciones primitivas del tipo IO \alpha\,, putChar y getChar, son específicas para entrada-salida.


Mónadas y la notación do

La clase Monad es nuestro primer ejemplo de una clase de tipos en el que la variable definida toma valor sobre constructores de tipos en lugar de sobre tipos. Para remarcar este hecho, utilizaremos letras normales en lugar de las letras griegas. El constructor de tipos IO es un ejemplo de mónada, para el cual se suministra como primitiva la correspondiente declaración de concreción.

Para las concreciones de la clase Monad, Haskell proporciona una notación alternativa - denominada notación do - para escribir combinaciones de >>= con cláusulas where anidadas. Por ejemplo:

readn         :: Int -> IO String
readn 0       = return []
readn (n + 1) = do c  <- getChar
                   cs <- readn n
                   return (c:cs)

El programa anterior se puede leer de la siguiente manera: Realiza primero la acción getChar y vincula el resultado a c; después realiza la acción readn n y vincula el resultado a cs; finalmente, devuelve (c:cs).

Podemos escribir una cláusula do con llaves y puntos y comas para separar explícitamente las cláusulas. Por ejemplo:

readn (n + 1) = do { c <- getChar; cs <- readn n; return (c:cs)}

Para una mónada M, una expresión do tiene la forma do {C; r} donde C es una lista de una o más acciones separadas por punto y coma, y r es una expresión de tipo \mbox{M }\beta\,, que es también el tipo de toda la expresión do. Cada orden tiene la forma x <- p, donde x es una variable o una tupla de variables; si p es una expresión de tipo \mbox{M }\alpha\,, entonces el tipo de x es \alpha\,. En el caso particular en que \alpha = 0\,, la orden ( ) <- p se puede abreviar a p.

La traducción de una expresión do a operaciones con >>= y cláusulas where se rige por dos reglas:

\mbox{do }{r} = r\,
\mbox{do }\{x\, \leftarrow \,p;\,C;\,r\} = p >>= q\mbox{ where }q\; x =\mbox{do }\{C;\, r\}\,


Operadores >>= y >>

En la sección anterior se ha introducido el operador >>=, no obstante también existe el operador >>. Entonces, ¿cuál es la diferencia entre >>= y >>?

En primer lugar veamos sus declaraciones:

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.


Funciones básicas de Entrada/Salida predefinidas

A continuación se muestran algunas de las funciones básicas de Entrada/Salida predefinidas:


Función Efecto
putChar :: Char -> IO () Imprime un carácter
putStr :: String -> IO () Imprime una cadena
putStrLn :: String -> IO () Imprime una cadena y un salto de línea
print :: Show a => a -> IO () Imprime un valor de cualquier tipo imprimible

(perteneciente a la clase Show).

getChar :: IO Char Lee un carácter
getLine :: IO String Lee una cadena de caracteres hasta que encuentra el

salto de línea.

getContents :: IO String Lee en una cadena toda la entrada del usuario (esta cadena

será potencialmente infinita y se podrá procesar gracias a la evaluación perezosa).

interact :: (String -> String) -> IO () Toma como argumento una función que procesa

una cadena y devuelve otra cadena. A dicha función se le pasa la entrada del usuario como argumento y el resultado devuelto se imprime.

writeFile :: String -> String -> IO Toma como argumentos el nombre de un fichero y una cadena,

y escribe dicha cadena en el fichero correspondiente.

appendFile :: String -> String -> IO () Toma como argumentos el nombre de un fichero y una

cadena, y añade dicha cadena al final del fichero correspondiente.

readFile :: String -> IO String Toma como argumento el nombre de un fichero y devuelve el

contenido en una cadena.


putChar

Función que imprime el carácter que se le pasa como argumento.

Recibe: un carácter.
Acción: escribe el carácter recibido.
Devuelve: nada.

A continuación se presentan tres ejemplos de uso sencillo de uso de la función putChar:

Hugs> putChar 'c'
c
Hugs> putChar '!'
!
Hugs> putChar '1'
1


putStr

Función que imprime la cadena que se le pasa como argumento.

Recibe: un String.
Acción: escribe el String recibido. Utiliza putChar para escribir los caracteres de uno en uno.
Devuelve: nada.


A continuación se presentan tres ejemplos de uso sencillo de uso de la función putStr:

Hugs> putStr "hola"
hola
Hugs> putStr "123"
123
Hugs> putStr ['a','b','c']
abc


putStrLn

Función que se basa en putStrLn y que imprime la cadena que se le pasa como argumento y añade un salto de línea al final.

Recibe: un String.
Acción: escribe el String recibido añadiendo un salto de línea al final. Utiliza putChar para escribir los caracteres de uno en uno.
Devuelve: nada.


A continuación se presentan tres ejemplos de uso sencillo de uso de la función putStrLn:

Hugs> putStrLn "hola"
hola
\n
Hugs> putStrLn "123"
123
\n
Hugs> putStrLn ['a','b','c']
abc
\n


print

Función que imprime un valor de cualquier tipo imprimible (perteneciente a la clase Show).

Recibe: un tipo imprimible perteneciente a la clase show.
Acción: imprime su argumento en la salida estándar.
Devuelve: nada.


A continuación se presentan tres ejemplos de uso sencillo de uso de la función print:

Hugs> print "Hola mundo"
"Hola mundo"
Hugs> print 121535
121535
Hugs> print ['a', 'b', 'c']
"abc"

getChar

Función que lee un carácter.

Recibe: nada.
Acción: lee un carácter.
Devuelve: el carácter leído.

A continuación se presentan ejemplos de uso sencillo de uso de la función getChar:

Hugs> getChar >>= putChar
cc :: IO()
Hugs> getChar >>= putChar
!! :: IO()
Hugs> getChar >>= putChar
11 :: IO()

En estos ejemplos vemos que se repite el carácter introducido, se debe a que el primer carácter que vemos se corresponde con lo que nosotros hemos introducido, y el segundo con la salida de la función putChar. Es decir, si el carácter que introducimos es A, entonces el de salida es A. Por ello:

Hugs> getChar >>= putChar
AA


getLine

Función que lee un string.

Recibe: nada.
Acción: lee una línea (\n).
Devuelve: el string leído.

A continuación se presentan ejemplos de uso sencillo de uso de la función getLine:

Hugs> getLine >>= putStr
hola\n
hola :: IO()
Hugs> getLine >>=  putStr
123\n
123 :: IO()
Hugs> getLine >>= putStr
abc\n
abc :: IO()


getContents

Esta función es igual que getLine pero empleando la evaluación "perezosa".

Recibe: nada.
Acción: lee una línea (\n).
Devuelve: el string leído.

A continuación se presentan ejemplos de uso sencillo de uso de la función getContents:

Hugs> getContents>>= putStr
eessttaa  ccaaddeennaa  eess  ppootteenncciiaallmmeennttee  iinnffiinniittaa{Interrupted!}

Nota: tengamos siempre en cuenta que nunca acabará la ejecución, puesto que por definición, la cadena que recibe es potencialmente infinita. Debemos abortar la ejecución. Realmente, lo que se introdujo en el ejemplo fue: esta cadena es potencialmente infinita


interact

Función que toma como argumento una función que procesa una cadena y devuelve otra cadena. A dicha función se le pasa la entrada del usuario como argumento y el resultado devuelto se imprime.

Recibe: una función de tipo String -> String..
Acción: lee un String del puerto de entrada, lo pasa a la función y el String resultado lo escribe en el puerto de salida..
Devuelve: nada.

A continuación se presentan ejemplos de uso sencillo de uso de la función interact:

Se va a calcular el factorial de cada uno de los números que componen una lista.

fact :: Integer -> Integer
fact 0 = 1
fact n = n * fact (n-1)
lfact :: String -> String
lfact  = unlines . procesa . lines 
procesa :: [String] -> [String]
procesa = map (show  . fact  .  read)

La ejecución normal sería de la forma:

Hugs> lfact "1\n2\n3\n4\n"
"1\n2\n6\n24\n" :: [Char]

Utilizando la función que nos ocupa:

Hugs> interact lfact
2
2
6
720
5
120
Hugs>


writeFile

Función que toma como argumentos el nombre de un fichero y una cadena, y escribe dicha cadena en el fichero correspondiente. Para escribir un valor que no sea de tipo String, debe convertirse previamente con la función show.

Recibe: un nombre de fichero y un String.
Acción: escribe el String recibido como segundo argumento en el fichero indicado. Presupone que existe y borra su contenido antes de escribir.
Devuelve: nada.

A continuación se presentan ejemplos de uso sencillo de uso de la función writeFile:

Implementación:

main = writeFile "/tmp/foo.txt" aaa
aaa  = "BBB" ++ "CCC"

Fichero: /tmp/foo.txt :

BBBCCC


appendFile

Toma como argumentos el nombre de un fichero y una cadena, y añade dicha cadena al final del fichero correspondiente. Hace lo mismo que writeFile pero añade el String al final del fichero, manteniendo el contenido anterior.

Recibe: un nombre de fichero y un String.
Acción: escribe el String recibido como segundo argumento en el fichero indicado, y añade dicha cadena al final del fichero correspondiente.
Devuelve: nada.

A continuación se presentan ejemplos de uso sencillo de uso de la función appendFile:

Ejemplo 1:

Fichero /Ejemplos/foo:

AAA

Implementación:

main = appendFile "/Ejemplos/foo.txt" aaa
aaa  = "BBB" ++ "CCC"

Fichero: /Ejemplos/foo.txt :

AAABBBCCC


readFile

Toma como argumento el nombre de un fichero y devuelve el contenido en una cadena. El fichero se lee según se necesita, de forma perezosa. En este sentido funciona igual que la función getContents.

Recibe: un nombre de fichero.
Acción: lee el fichero indicado.
Devuelve: un String con el contenido del fichero.

A continuación se presentan ejemplos de uso sencillo de uso de la función readFile:

Ejemplo 1:

Fichero /Ejemplos/foo: 
BBBCCC

Implementación y salida:

main = do x <- readFile "/tmp/foo.txt"
putStr x
Salida: BBBCCC

Ejemplo 2:

Fichero /Ejemplos/foo.txt: 
[1,2,4,6,7]

Implementación y salida:

main = do x <- readFile "/tmp/foo.txt"
y <- rList x
print (sum y)
rList :: String -> IO [Int]	  
rList = readIO
Salida: 20

Control de excepciones

El sistema incluye un mecanismo simple de control de excepciones. Cualquier operación de Entrada/Salida podría lanzar una excepción en lugar de devolver un resultado.

Las excepciones se representan como valores de tipo IOError. Este tipo representa todas las posibles excepciones que pueden ocurrir al ejecutar operaciones de la mónada de E/S. El tipo es abstracto: los constructores no están disponibles para el usuario. Algunos predicados permiten inspeccionar datos del tipo IOError.

Por ejemplo, la función

isEOFError :: IOError -> Bool 

determina si el error que toma como argumento fue causado por una condición de final de fichero. Al ser el tipo abstracto, los diseñadores del lenguaje pueden añadir nuevos tipos de errores al sistema sin implicar el cambio en la implementación del tipo.

Mediantela función userError el usuario podría lanzar también sus propios errores.

Las excepciones pueden ser lanzadas y capturadas mediante las funciones:

fail :: IOError -> IO a
catch :: IO a -> (IOError -> IO a) -> IO a

La función fail lanza una excepción; la función catch establece un manejador que recibe cualquier excepción elevada en la acción protegida por él. Una excepción es capturada por el manejador más reciente. Una excepción es capturada por el manejador más reciente. Puesto que los manejadores capturan todas las excepciones (no son selectivos), el programador debe encargarse de propagar las excepciones que no desea manejar. Si una excepción se propaga fuera del sistema, si imprime un error de tipo IOError.

Ejemplo de uso catch para tratar los errores provocados por el final de un fichero:

getChar'           :: IO Char
getChar'           =  getChar `catch` eofHandler where
 eofHandler e = if isEofError e then return '\n' else ioError e
getLine'        :: IO String
getLine'        = catch getLine (\err -> return ("Error: " ++ show err)) where
                  getLine = do c <- getChar'
                                 if c == '\n' then return ""
                                              else do l <- getLine'
                                                      return (c:l) 

El manejador de excepciones anidado permite que getChar' trate los errores de fin de fichero, mientras que otros errores hacen que getLine' devuelva una cadena de caracteres comenzado por "Error:".

Haskell define un manejador de excepciones por defecto en el nivel más externo de un programa cuyo comportamiento consiste en imprimir un mensaje con la excepción producida y finalizar el programa.

Ejemplo

Vamos leer el contenido escrito en minúsculas de un fichero y lo guardaremos en otro fichero convertido a mayúsculas.

import Char;
main = readFile "C:\\fentrada.txt" >>= \cad ->
       writeFile "C:\\fsalida.txt" (map toUpper cad) >> 
       putStr "Contenido fichero de entrada:\n" >> putStrLn (cad ++ "\n")>>
       putStrLn "Conversion realizada\n" >> 
       putStr "Contenido fichero de salida:\n" >>
       readFile "C:\\fsalida.txt" >>= \cad -> putStrLn cad

Para que tenga éxito el programa deben existir los ficheros fentrada.txt y fsalida.txt en la ruta de C: previamente.

Bibliografía

Herramientas personales