I'm a complete neophyte to Haskell with only a smidge of FP experience – I did some Racket programming academically and have written some semi-FP JavaScript professionally, but now I'm trying to learn Haskell (and FP better). So I read Haskell Fast & Hard with no problems up until the last chapter.
Monads.
Make a program that sums all of its arguments. Hint: use the function
getArgs
.
I eventually wrote a program that nominally achieves this requirement, but it felt very hacky. For reference, here is a "correct" program by the author that I think I was supposed to model after. It sums the numbers in a comma-delimited string.
import Data.Maybe
maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
[(x,"")] -> Just x
_ -> Nothing
getListFromString :: String -> Maybe [Integer]
getListFromString str = maybeRead $ "[" ++ str ++ "]"
askUser :: IO [Integer]
askUser = do
putStrLn "Enter a list of numbers (separated by comma):"
input <- getLine
let maybeList = getListFromString input in
case maybeList of
Just l -> return l
Nothing -> askUser
-- show
main :: IO ()
main = do
list <- askUser
print $ sum list
Based on the above approach, I wrote a function to convert the list of argument-strings back into a comma-delimited string so that most of the other functions could work unchanged.
import Data.Maybe
import System.Environment (getArgs)
maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
[(x,"")] -> Just x
_ -> Nothing
getListFromString :: String -> Maybe [Integer]
getListFromString str = maybeRead $ "[" ++ str ++ "]"
getArgsAsString :: [String] -> String
getArgsAsString [] = "0"
getArgsAsString (x:xs) = x ++ "," ++ getArgsAsString xs
askUser :: [String] -> [Integer]
askUser input = case (getListFromString (getArgsAsString input)) of
Just l -> l
Nothing -> error "failsauce"
main :: IO ()
main = do
input <- getArgs
print $ sum (askUser input)
I am trying to stick to the spirit of the exercise and not go too far into library functions that solve the problem for me without teaching me anything (in particular, the author says not to try too hard to understand the syntax of maybeRead
), but it feels very wrong to convert a list of strings to a comma-separated string only to split it out again into a maybe-list of integers.
All advice is appreciated! (But educational guidance is appreciated more than "just use xyz lib function".)
2 Answers 2
There's very little I would salvage from the model code.
- Converting the arguments list into comma-separated string is a little hackish and not very efficient (nevertheless it works, and I'd be totally OK with it for a quick throw away script). Therefore I would not use
getListFromString
. askUser
is not needed since we're not interacting with the user.- There's already an alternative to
maybeRead
, inText.Read
calledreadMaybe
.
This is how I would implement it:
import System.Environment (getArgs)
import Text.Read (readMaybe)
main :: IO ()
main = do
values <- fmap (map readMaybe) getArgs
putStrLn $ maybe "Arguments invalid" (show . sum) $ sequence values
-
\$\begingroup\$ SuggestIon: instead of using
fmap
andsequence
like that, just usedo { values <- getArgs ; putStrLn $ ... $ mapM readMaybe values }
. This is immediately clearer, in my opinion; you only work in one monad at a time. If you prefer pointfree-golf, this can also be written as the one-linergetArgs >>= putStrLn . maybe "Arguments invalid" (show . sum) . mapM readMaybe
. \$\endgroup\$wchargin– wchargin2015年11月26日 22:32:34 +00:00Commented Nov 26, 2015 at 22:32
The reference solution is already a bit flawed: It depends on the fact that the standard library happens to use commas to separate elements when pretty-printing lists. That's the only reason it can just throw brackets on the string and then run it through reads
(these functions reconstruct the data structure from its pretty-printed form, e.g. read "[1,2]" -> [1,2]
). If the question was about a semicolon-separated list, this approach would need significant modification.
Building on top of this is a complete waste, I would suggest you approach the problem with a fresh mind instead. Your problem isn't complicated - you get a list of String
s from getArgs
, and want a list of Integer
s to pass to sum
. Do you know how to read an Integer
from a String
? Do you know how to apply an operation to all elements of a list? That's really all you need here.
-
1\$\begingroup\$ It's how it's defined in the Haskell Report, so I'd say that using
read
to parse a comma-separated list is fair game, although not very efficient. \$\endgroup\$Pedro Rodrigues– Pedro Rodrigues2014年01月03日 14:36:06 +00:00Commented Jan 3, 2014 at 14:36 -
1\$\begingroup\$ Sure - my point was that it's inflexible. Hard-coding the comma separation like this is simply bad style. \$\endgroup\$Peter Wortmann– Peter Wortmann2014年01月05日 16:11:20 +00:00Commented Jan 5, 2014 at 16:11
Explore related questions
See similar questions with these tags.
print $ sum $ map read input
\$\endgroup\$