I am trying to use this module to create a command line tool that will grab a quote from yahoo finance for a symbol. When I try to compile I receive this error.
Couldn't match expected type `[t0]'
with actual type `IO
(Maybe (Map (QuoteSymbol, QuoteField) QuoteValue))'
In the return type of a call of `getQuote'
In a stmt of a 'do' expression:
q <- getQuote [arg] ["s", "l1", "c"]
In the expression:
do { q <- getQuote [arg] ["s", "l1", ....];
case q of {
Nothing -> error "symbol not found"
Just m -> m } }
I am just starting to learn haskell, which is an incredibly different but powerful language, and I can not quite figure out what the problem is. Any help would be greatly appreciated and I would also appreciate any feedback on if I'm doing this the "haskell" way and if not any improvements I can make. Thanks!
module Main where
import Finance.Quote.Yahoo
import Data.Time.Calendar
import Data.Map
import System( getArgs )
import System.Console.GetOpt
import Data.Maybe ( fromMaybe )
data Options = Options
{ optVerbose :: Bool
, optShowVersion :: Bool
, optOutput :: Maybe FilePath
, optInput :: Maybe FilePath
, optLibDirs :: [FilePath]
, optSymbol :: String
} deriving Show
defaultOptions = Options
{ optVerbose = False
, optShowVersion = False
, optOutput = Nothing
, optInput = Nothing
, optLibDirs = []
, optSymbol = "YHOO"
}
options :: [OptDescr (Options -> Options)]
options =
[ Option ['v'] ["verbose"]
(NoArg (\ opts -> opts { optVerbose = True }))
"chatty output on stderr"
, Option ['V','?'] ["version"]
(NoArg (\ opts -> opts { optShowVersion = True }))
"show version number"
, Option ['o'] ["output"]
(OptArg ((\ f opts -> opts { optOutput = Just f }) . fromMaybe "output")
"FILE")
"output FILE"
, Option ['c'] []
(OptArg ((\ f opts -> opts { optInput = Just f }) . fromMaybe "input")
"FILE")
"input FILE"
, Option ['L'] ["libdir"]
(ReqArg (\ d opts -> opts { optLibDirs = optLibDirs opts ++ [d] }) "DIR")
"library directory"
, Option ['s'] ["symbol"]
(ReqArg (\ s opts -> opts { optSymbol = getSymbol s }) "SYMBOL")
"symbol SYMBOL"
]
compilerOpts :: [String] -> IO (Options, [String])
compilerOpts argv =
case getOpt Permute options argv of
(o,n,[] ) -> return (foldl (flip id) defaultOptions o, n)
(_,_,errs) -> ioError (userError (concat errs ++ usageInfo header options))
where header = "Usage: ic [OPTION...] files..."
main = do
args <- getArgs
compilerOpts args
getSymbol :: String -> String
getSymbol arg = do
q <- getQuote [arg] ["s", "l1", "c"]
case q of
Nothing -> error "symbol not found"
Just m -> m
The trouble is that getSymbol has no business in the definition of options, since it is an IO action as we can see from the fact that it uses getQuote:
getQuote :: [QuoteSymbol]
-> [QuoteField]
-> IO (Maybe (Map (QuoteSymbol, QuoteField) QuoteValue))
So where you say optSymbol = getSymbol s you should just put the string s. The crucial line should certainly read
(ReqArg (\ s opts -> opts { optSymbol = s }) "SYMBOL")
not
(ReqArg (\ s opts -> opts { optSymbol = getSymbol s }) "SYMBOL")
At the moment you are just parsing arguments. What you do with the optSymbol string -- e.g. go to Yahoo to find out about it via getSymbol -- belongs in main or some preliminary action you need to define. Here is a module that typechecks, it barely does anything, acting like this:
-- $ ./yahoo -s STD.F -- Standard Chartered?
-- STD.F c -0.396 - -1.99%
-- STD.F l1 19.492
-- STD.F s STD.F
module Main where
import Finance.Quote.Yahoo
import Data.Time.Calendar
import Data.Map
import Prelude
import qualified Prelude
import System.Environment( getArgs )
import System.Console.GetOpt
import Data.Maybe ( fromMaybe )
import System.IO.Unsafe
main = do
args <- getArgs
(options, strs) <- compilerOpts args
whatIDoWithTheUsersOptions options strs
-- This is not doing much with all this user input ...
whatIDoWithTheUsersOptions :: Options -> [String] -> IO ()
whatIDoWithTheUsersOptions options strs = do
blather <- getSymbol $ optSymbol options
putStrLn blather
getSymbol :: String -> IO String
getSymbol arg = do
q <- getQuote [arg] ["s", "l1", "c"]
case q of
Nothing -> return $ "symbol " ++ arg ++ " not found"
Just m -> return $ unlines $ Prelude.map helper (toList m)
where helper ((a,b), c) = unwords [a,b,c]
data Options = Options
{ optVerbose :: Bool
, optShowVersion :: Bool
, optOutput :: Maybe FilePath
, optInput :: Maybe FilePath
, optLibDirs :: [FilePath]
, optSymbol :: String
} deriving Show
defaultOptions = Options
{ optVerbose = False
, optShowVersion = False
, optOutput = Nothing
, optInput = Nothing
, optLibDirs = []
, optSymbol = "YHOO"
}
options :: [OptDescr (Options -> Options)]
options =
[ Option ['v'] ["verbose"]
(NoArg (\ opts -> opts { optVerbose = True }))
"chatty output on stderr"
, Option ['V','?'] ["version"]
(NoArg (\ opts -> opts { optShowVersion = True }))
"show version number"
, Option ['o'] ["output"]
(OptArg ((\ f opts -> opts { optOutput = Just f }) . fromMaybe "output")
"FILE")
"output FILE"
, Option ['c'] []
(OptArg ((\ f opts -> opts { optInput = Just f }) . fromMaybe "input")
"FILE")
"input FILE"
, Option ['L'] ["libdir"]
(ReqArg (\ d opts -> opts { optLibDirs = optLibDirs opts ++ [d] }) "DIR")
"library directory"
, Option ['s'] ["symbol"]
(ReqArg (\ s opts -> opts { optSymbol = s }) "SYMBOL")
"symbol SYMBOL"
]
compilerOpts :: [String] -> IO (Options, [String])
compilerOpts argv =
case getOpt Permute options argv of
(o,n,[] ) -> return (Prelude.foldl (flip id) defaultOptions o, n)
(_,_,errs) -> ioError (userError (concat errs ++ usageInfo header options))
where header = "Usage: ic [OPTION...] files..."
This is wrong:
getSymbol :: String -> String
getSymbol arg = do
q <- getQuote [arg] ["s", "l1", "c"]
case q of
Nothing -> error "symbol not found"
Just m -> m
It should probably be:
getSymbol :: String -> IO String
getSymbol arg = do
q <- getQuote [arg] ["s", "l1", "c"]
case q of
Nothing -> fail "symbol not found"
Just m -> return m
Note I have changed the type signature and the final two lines. (Since I have changed the type of getSymbol, you will have to use the result differently wherever you call it.)
A partial explanation:
You are using do syntax, which results in a monadic value. The return type of getSymbol was String, which is the same as [Char]. Lists are monads, so this is fine.
But then you call getQuote on the right of a <-, which results in an IO value. IO is monadic, but IO is not the same as lists, so we have an error. (You have to use the same monad on the right hand side of a <- as the do block results in.)
Edit: The above code change isn't going to be enough. m has type Map (QuoteSymbol, QuoteField) QuoteValue which obviously isn't the same as String, so you want to compute something from m instead of returning it directly. I don't know exactly what you want to do here.
Also, you're using getSymbol in your option parsing code. It's better if your save the input to getSymbol in your Options, and call getSymbol later with that value, after you have processed the command line options. (Otherwise you might contact Yahoo's servers only to discover that a different command line option is wrong and throw the answer away. Not to mention that the optSymbol field can't hold IO.)
Related
Hello please can someone tell me where this error comes from ?
The error is
DKA-2-MKA.hs:39:5:
The last statement in a 'do' block must be an expression
argv <- getArgs
i checked intendation already and it didnt helped
import System.Environment
import System.Console.GetOpt
import Data.Maybe ( fromMaybe )
data Options = Options { optI :: Boo , optT :: Bool } deriving (Show)
defaultOptions :: Options
defaultOptions = Options { optI = False, optT = False }
options :: [OptDescr (Options -> Options)]
options = [ Option ['i'] ["I"]
(NoArg (\ opts -> opts {optI = True}))
"I is true"
, Option ['t'] ["T"]
(NoArg (\ opts -> opts {optT = True}))
"T is true"
]
programOpt :: [String] -> IO (Options, String)
programOpt argv =
case getOpt Permute options argv of
(o, n, [] ) -> return (foldl (flip id) defaultOptions o, n)
(o, _, [] ) -> ioError $ userError $ exactlyone ++ usageInfo header options
(_, _, errs) -> ioError $ userError $ concat errs ++ usageInfo header options
where
header = "Usage [OPTIONS...] filename"
exactlyone = "One input"
main :: IO ()
main = do
argv <-getArgs
DKA-2-MKA.hs:39:5: The last statement in a 'do' block must be an expression argv <- getArgs
39:5 indicates the error is at line 39, column 5. Which looks like argv <- getArgs
an "expression" is a bit of code that is syntactically valid, has a type, and can be evaluated. Examples of expressions include
let x = 1 in x + 2
if b then x else y
5
return ()
examples of not expressions include:
(1
let x = 5
if b then x
argv <-getArgs is not an expression, it's a dangling bit of syntax that doesn't make sense on its own; you can't ask ghci :t argv <-getArgs.
In order to understand why the last line in a do block must be an expression, search "desugaring do notation haskell" on this site or elsewhere (maybe someone else can recommend a specific resource). This will also be in any intro haskell book.
I have a
foobar :: IO (ParseResult [(String,String)])
ParseResult is a monad defined here: https://hackage.haskell.org/package/haskell-src-exts-1.13.5/docs/Language-Haskell-Exts-Parser.html#t:ParseResult
I want to take those strings and write them to a LaTeXT m () defined in https://hackage.haskell.org/package/HaTeX-3.17.1.0/docs/Text-LaTeX-Base-Writer.html
Running this function results in no file being created.
writeReport2 :: [Char] -> IO (ParseResult (IO ()))
writeReport2 name = do x <- foobar
return $ do y <- x
return $ do z <- (execLaTeXT.docAndGraph) y
renderFile fileName z
where
fileName = name ++ ".tex"
However the code:
writeReport :: t -> LaTeXT IO a -> IO ()
writeReport name report = createLatex >>= renderFile fileName
where
createLatex = execLaTeXT report
fileName = "AAAAA" ++ ".tex"
testFoo = [(" | HaskellExample Example File\n | Two examples are given below:\n\n >>> fib 10\n 55\n\n >>> putStrLn \"foo\\nbar\"\n foo\n bar ","fib :: Int -> Int"),("\n | This is a thing: ","fib = undefined"),("\n | This is a thing:\n","fibar :: String -> Float")]
itWorks = writeReport "AAAA.txt" $ docAndGraph testFoo
Will create a new file.
Both sets of code type check.
I could get writeReport2 working without modification.
I think what might have been your problem is the nested IO action in the return value of writeResport2!
In order to flatten the nested IO actions, I had to use the function join :: Monad m => m (m a) -> m a from Control.Monad:
main :: IO ()
main = join $ fromParseResult <$> writeReport2 "test"
Here is my complete code:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Language.Haskell.Exts.Parser
import Text.LaTeX.Base.Writer
import Text.LaTeX
import Data.String
import Control.Monad
foobar :: IO (ParseResult [(String, String)])
foobar = return (ParseOk testFoo)
testFoo = [ ( " | HaskellExample Example File\n | Two examples are given below:\n\n >>> fib 10\n 55\n\n >>> putStrLn \"foo\\nbar\"\n foo\n bar "
, "fib :: Int -> Int"
)
, ("\n | This is a thing: ", "fib = undefined")
, ("\n | This is a thing:\n", "fibar :: String -> Float")
]
docAndGraph :: Monad m => [(String, String)] -> LaTeXT m ()
docAndGraph x = do
documentclass [] article
document $
raw (fromString (show x))
writeReport2 :: [Char] -> IO (ParseResult (IO ()))
writeReport2 name = do
x <- foobar
return $ do
y <- x
return $ do
z <- (execLaTeXT . docAndGraph) y
renderFile fileName z
where
fileName = name ++ ".tex"
main :: IO ()
main = join $ fromParseResult <$> writeReport2 "test"
Loading into GHCi:
$ stack ghci
io-action-nested-in-other-monads-not-executing-0.1.0.0: initial-build-steps (exe)
Configuring GHCi with the following packages: io-action-nested-in-other-monads-not-executing
Using main module: 1. Package `io-action-nested-in-other-monads-not-executing' component exe:io-action-nested-in-other-monads-not-executing with main-is file: /home/sven/dev/stackoverflow-questions/io-action-nested-in-other-monads-not-executing/src/Main.hs
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /home/sven/.ghc/ghci.conf
[1 of 1] Compiling Main ( /home/sven/dev/stackoverflow-questions/io-action-nested-in-other-monads-not-executing/src/Main.hs, interpreted )
Ok, modules loaded: Main.
Loaded GHCi configuration from /tmp/ghci22616/ghci-script
And running it:
λ main
Creates this file:
$ cat test.tex
\documentclass{article}\begin{document}[(" | HaskellExample Example File\n | Two examples are given below:\n\n >>> fib 10\n 55\n\n >>> putStrLn \"foo\\nbar\"\n foo\n bar ","fib :: Int -> Int"),("\n | This is a thing: ","fib = undefined"),("\n | This is a thing:\n","fibar :: String -> Float")]\end{document}%
I know it is not the scope of the question, but you could circumvent the nested IO if you want, by doinf this, for example:
writeReport3 :: [Char] -> IO ()
writeReport3 name = do
let fileName = name ++ ".tex"
x <- foobar
case x of
ParseOk y -> do
z <- execLaTeXT (docAndGraph y)
renderFile fileName z
ParseFailed _ _ ->
return ()
main :: IO ()
main = writeReport3 "test"
In the second example given in the documentation for System.Console.GetOpt, reproduced here, I am unable to understand or unpack this line:
(o,n,[] ) -> return (foldl (flip id) defaultOptions o, n)
What is this foldl doing, and how does it achieve it? What is the purpose of (flip id)? What is going on?
Code:
import System.Console.GetOpt
import Data.Maybe ( fromMaybe )
data Options = Options
{ optVerbose :: Bool
, optShowVersion :: Bool
, optOutput :: Maybe FilePath
, optInput :: Maybe FilePath
, optLibDirs :: [FilePath]
} deriving Show
defaultOptions = Options
{ optVerbose = False
, optShowVersion = False
, optOutput = Nothing
, optInput = Nothing
, optLibDirs = []
}
options :: [OptDescr (Options -> Options)]
options =
[ Option ['v'] ["verbose"]
(NoArg (\ opts -> opts { optVerbose = True }))
"chatty output on stderr"
, Option ['V','?'] ["version"]
(NoArg (\ opts -> opts { optShowVersion = True }))
"show version number"
, Option ['o'] ["output"]
(OptArg ((\ f opts -> opts { optOutput = Just f }) . fromMaybe "output")
"FILE")
"output FILE"
, Option ['c'] []
(OptArg ((\ f opts -> opts { optInput = Just f }) . fromMaybe "input")
"FILE")
"input FILE"
, Option ['L'] ["libdir"]
(ReqArg (\ d opts -> opts { optLibDirs = optLibDirs opts ++ [d] }) "DIR")
"library directory"
]
compilerOpts :: [String] -> IO (Options, [String])
compilerOpts argv =
case getOpt Permute options argv of
(o,n,[] ) -> return (foldl (flip id) defaultOptions o, n)
(_,_,errs) -> ioError (userError (concat errs ++ usageInfo header options))
where header = "Usage: ic [OPTION...] files..."
The type of flip id is b -> (b -> c) -> c and you could find explanations here: Why does Haskell's "flip id" has this type?
The foldl (flip id) defaultOptions o sub-expression does the following:
Takes defaultOptions as initial value (defaultOptions has type Options)
Takes each element from o (each element has type Options -> Options)
Folds all elements using flip id function (it has b -> (b -> c) -> c type)
Since all of o elements changes corresponding option in the given configuration, the result of the foldl (flip id) defaultOptions o will be a configuration of all parsed options. All missed options replaced with their default values from defaultOptions.
The other parts of (o,n,[] ) -> return (foldl (flip id) defaultOptions o, n) expressions are pretty simple:
(o,n,[] ) -> matches a list of parsed options, a list of non-options and an empty list of errors
return (..., n) just puts the value (..., n) into monad IO (Options, [String])
(This is not strictly an answer to your question, but #soon asked me to post it anyway.)
The semantics of each command-line argument you define is described by
a transition function Options -> Options. Since you can pass many
arguments to a program, you end up with a list of such transition
functions [Options -> Options]. The goal is to compute the sum
effect of these transitions, i.e. an Options -> Options that applies
each transition in turn.
A particularly nice way of achieving this is to observe the structure
of endomorphisms a -> a for any type a:
id :: a -> a is the identity transition function that doesn't actually do anything
Given two transition functions f1, f2 :: a -> a, their composition
f1 . f2 corresponds precisely to applying both, in order. Note
also that this combination is associative, since doing f2 . f3 and
then f1 is the same as doing f3 followed by f1 . f2.
So we have a monoid!
The Haskell standard library base already contains this monoid with
the name Endo.
Using this, we can rewrite
foldl (flip id) defaultOptions o
in a much nicer way that, in my opinion, makes it immediately obvious what's happening here:
appEndo (fold o) defaultOptions
by changing the result type of options to [OptDescr (Endo Options)]; or, if you'd rather not, you can just add the extra Endo
line noise at combination time (by writing appEndo (foldMap Endo o) defaultOptions). Here, fold o :: Endo Options is the composite of all the individual transition functions, and appEndo (fold o) :: Options -> Options is how this resulting transition function is finally applied to the initial Options.
Note that this also works regardless of the data structure used for o: it will work for a list of transition functions, or a tree, or a Maybe, because of the associative property of the monoid at hand; and fold is polymorphic enough to expose this.
I am trying to teach myself Haskell. As a sample program, I am writing a Spider solitaire player.
I am trying to write a command line parser using System.Console.GetOpt. I know there are easier ways to do argument parsing for this program, but I want to learn how to use the GetOpt module because I anticipate needing its sophistication later in other programs that I will be writing.
I am trying to add a "--help" option that just prints a usage message and then exits. I would also like to print usage messages if either of the arguments to the "--games" option or the "--suits" option are not valid integers (games >= 1 and <= 1000, suits == 1, 2, or 4). I will be passing the resulting Options data type to other parts of my program.
I am also getting an error that progName is not in scope. Isn't the case statement in parseArgs in the scope of the do block?
Here is my code, patched together from the examples in "Real World Haskell" and the Haskell wiki:
module Main (main) where
import System.Console.GetOpt
import System.Environment(getArgs, getProgName)
data Options = Options {
optGames :: Int
, optSuits :: Int
, optVerbose :: Bool
} deriving Show
defaultOptions = Options {
optGames = 1
, optSuits = 4
, optVerbose = False
}
options :: [OptDescr (Options -> Options)]
options =
[ Option ['g'] ["games"]
(ReqArg (\g opts -> opts { optGames = (read g) }) "GAMES")
"number of games"
, Option ['s'] ["suits"]
(ReqArg (\s opts -> opts { optSuits = (read s) }) "SUITS")
"number of suits"
, Option ['v'] ["verbose"]
(NoArg (\opts -> opts { optVerbose = True }))
"verbose output"
]
parseArgs :: IO Options
parseArgs = do
argv <- getArgs
progName <- getProgName
case getOpt RequireOrder options argv of
(opts, [], []) -> return (foldl (flip id) defaultOptions opts)
(_, _, errs) -> ioError (userError (concat errs ++ helpMessage))
where
header = "Usage: " ++ progName ++ " [OPTION...]"
helpMessage = usageInfo header options
main :: IO ()
main = do
options <- parseArgs
putStrLn $ show options
Here is the solution that I came up with:
module Main (main) where
import Control.Monad
import Control.Monad.Error
import System.Console.GetOpt
import System.Environment(getArgs, getProgName)
data Options = Options {
optGames :: Int
, optSuits :: Int
, optVerbose :: Bool
} deriving Show
defaultOptions = Options {
optGames = 1
, optSuits = 4
, optVerbose = False
}
options :: [OptDescr (Options -> Either String Options)]
options =
[ Option ['g'] ["games"]
(ReqArg (\g opts ->
case reads g of
[(games, "")] | games >= 1 && games <= 1000 -> Right opts { optGames = games }
_ -> Left "--games must be a number between 1 and 1000"
) "GAMES")
"number of games"
, Option ['s'] ["suits"]
(ReqArg (\s opts ->
case reads s of
[(suits, "")] | suits `elem` [1, 2, 4] -> Right opts { optSuits = suits }
_ -> Left "--suits must be 1, 2, or 4"
) "SUITS")
"number of suits"
, Option ['v'] ["verbose"]
(NoArg (\opts -> Right opts { optVerbose = True }))
"verbose output"
]
parseArgs :: IO Options
parseArgs = do
argv <- getArgs
progName <- getProgName
let header = "Usage: " ++ progName ++ " [OPTION...]"
let helpMessage = usageInfo header options
case getOpt RequireOrder options argv of
(opts, [], []) ->
case foldM (flip id) defaultOptions opts of
Right opts -> return opts
Left errorMessage -> ioError (userError (errorMessage ++ "\n" ++ helpMessage))
(_, _, errs) -> ioError (userError (concat errs ++ helpMessage))
main :: IO ()
main = do
options <- parseArgs
putStrLn $ show options
How can I improve this?
I've written a simple XML parser in Haskell.
The function convertXML recieves contents of a XML file and returns a list of extracted values that are further processed.
One attribute of XML tag contains also an URL of a product image and I would like to extend the function to also download it if the tag is found.
convertXML :: (Text.XML.Light.Lexer.XmlSource s) => s -> [String]
convertXML xml = productToCSV products
where
productToCSV [] = []
productToCSV (x:xs) = (getFields x) ++ (productToCSV
(elChildren x)) ++ (productToCSV xs)
getFields elm = case (qName . elName) elm of
"product" -> [attrField "uid", attrField "code"]
"name" -> [trim $ strContent elm]
"annotation" -> [trim $ strContent elm]
"text" -> [trim $ strContent elm]
"category" -> [attrField "uid", attrField "name"]
"manufacturer" -> [attrField "uid",
attrField "name"]
"file" -> [getImgName]
_ -> []
where
attrField fldName = trim . fromJust $
findAttr (unqual fldName) elm
getImgName = if (map toUpper $ attrField "type") == "FULL"
then
-- here I need some IO code
-- to download an image
-- fetchFile :: String -> IO String
attrField "file"
else []
products = findElements (unqual "product") productsTree
productsTree = fromJust $ findElement (unqual "products") xmlTree
xmlTree = fromJust $ parseXMLDoc xml
Any idea how to insert an IO code in the getImgName function or do I have to completely rewrite convertXML function to an impure version ?
UPDATE II
Final version of convertXML function. Hybrid pure/impure but clean way suggested by Carl. Second parameter of returned pair is an IO action that runs images downloading and saving to disk and wraps list of local paths where are images stored.
convertXML :: (Text.XML.Light.Lexer.XmlSource s) => s -> ([String], IO [String])
convertXML xml = productToCSV products (return [])
where
productToCSV :: [Element] -> IO String -> ([String], IO [String])
productToCSV [] _ = ([], return [])
productToCSV (x:xs) (ys) = storeFields (getFields x)
( storeFields (productToCSV (elChildren x) (return []))
(productToCSV xs ys) )
getFields elm = case (qName . elName) elm of
"product" -> ([attrField "uid", attrField "code"], return [])
"name" -> ([trim $ strContent elm], return [])
"annotation" -> ([trim $ strContent elm], return [])
"text" -> ([trim $ strContent elm], return [])
"category" -> ([attrField "uid", attrField "name"], return [])
"manufacturer" -> ([attrField "uid",
attrField "name"], return [])
"file" -> getImg
_ -> ([], return [])
where
attrField fldName = trim . fromJust $
findAttr (unqual fldName) elm
getImg = if (map toUpper $ attrField "type") == "FULL"
then
( [attrField "file"], fetchFile url >>=
saveFile localPath >>
return [localPath] )
else ([], return [])
where
fName = attrField "file"
localPath = imagesDir ++ "/" ++ fName
url = attrField "folderUrl" ++ "/" ++ fName
storeFields (x1s, y1s) (x2s, y2s) = (x1s ++ x2s, liftM2 (++) y1s y2s)
products = findElements (unqual "product") productsTree
productsTree = fromJust $ findElement (unqual "products") xmlTree
xmlTree = fromJust $ parseXMLDoc xml
The better approach would be to have the function return the list of files to download as part of the result:
convertXML :: (Text.XML.Light.Lexer.XmlSource s) => s -> ([String], [URL])
and download them in a separate function.
The entire point of the type system in Haskell is that you can't do IO except with IO actions - values of type IO a. There are ways to violate this, but they run the risk of behaving entirely unlike what you'd expect, due to interactions with optimizations and lazy evaluation. So until you understand why IO works the way it does, don't try to make it work differently.
But a very important consequence of this design is that IO actions are first class. With a bit of cleverness, you could write your function as this:
convertXML :: (Text.XML.Light.Lexer.XmlSource s) => s -> ([String], IO [Image])
The second item in the pair would be an IO action that, when executed, would give a list of the images present. That would avoid the need to have image loading code outside of convertXML, and it would allow you to do IO only if you actually needed the images.
I basically see to approaches:
let the function give out a list of found images too and process them with an impure function afterwards. Laziness will do the rest.
Make the whole beast impure
I generally like the first approach more. d