3
\$\begingroup\$

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".)

\$\endgroup\$
  • \$\begingroup\$ If all you want to do on an invalid input is to raise an error with an inane message, then you can be done with print $ sum $ map read input \$\endgroup\$ – abuzittin gillifirca Jan 2 '14 at 12:22
7
\$\begingroup\$

There's very little I would salvage from the model code.

  1. 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.
  2. askUser is not needed since we're not interacting with the user.
  3. There's already an alternative to maybeRead, in Text.Read called readMaybe.

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
\$\endgroup\$
  • \$\begingroup\$ SuggestIon: instead of using fmap and sequence like that, just use do { 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-liner getArgs >>= putStrLn . maybe "Arguments invalid" (show . sum) . mapM readMaybe. \$\endgroup\$ – wchargin Nov 26 '15 at 22:32
5
\$\begingroup\$

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 Strings from getArgs, and want a list of Integers 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.

\$\endgroup\$
  • 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 Jan 3 '14 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 Jan 5 '14 at 16:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.