Search code examples
listhaskellhexreadfilemaplist

Haskell list has is showing number and empty string when mapping


I am reading a file, this file contains hexadecimal bytes. I read this into a string, I map them into single words and then I turn them into their decimal values.

main = do
  args <- getArgs
  contents <- readFile (head args)
  let singlewords = words contents
  let intContents = map readHex singlewords

When I print the output of intContents, I get:

[[(5,"")],[(0,"")],[(0,"")],[(0,"")],[(5,"")],[(0,"")],[(0,"")],[(0,"")],[(48,"")],[(28,"")],[(75,"")],[(201,"")],[(134,"")],[(0,"")],[(0,"")]]

These are the proper decimal values, but why are they paired with an empty string? and as separate nested lists? I just want the values like:

[5,0,0,0,5,...]

I tried to just do:

intContents <- readHex contents

But I still only get:

[(5," 00 00 00 05 00 00 00\n30 1C 4B C9 86 00 00")]

This just does the first term. Any help would be appreciated.

Edit

Now understanding the types readHex returns, how can I use pattern matching with my code to get just the decimal number.

Some sort of definition like this?

intContents :: [(a, b)] -> a
intContents [(a, b)] = a

or do I just put the pattern in the mapping like this?

let intContents = map ([(a, b)] (readHex singleWords)

Note: I tried both of these and just have errors I don't necessarily understand

Again any help is appreciated!


Solution

  • how can I use pattern matching with my code to get just the decimal number.

    It can't be done, in general, because sometimes there will be no decimal number! For example:

    > readHex "lol"
    []
    

    So to do a good job of this task, you must decide what you want to happen in surprising cases. This is, in my opinion, the type system helping you to think about what code needs to be written.

    One choice you could make would be like this: if any of the contents of the file aren't hex numbers, print a message saying so and exit without doing anything else. This is about the coarsest-grained response you could have; to accomplish this task we need only remember whether there was an error so far or not. We can do this using Maybe; as usual with lists, the patterns we need to match are [] and _:_. So:

    readHexMaybe :: String -> Maybe Int
    readHexMaybe s = case readHex s of
        [] -> Nothing
        (n, s') : otherResults -> Just n
    

    If you write this code, and turn on warnings, you'll get told that s' and otherResults aren't used. And that does seem like something worth thinking about! In the s' position, a successful parse will give us an empty string; and in the otherResults position, a successful parse will give us an empty list. We should think about what to do in other cases -- the type system helping us again!

    Continuing our plan of demanding that everything go perfectly, we could expand this:

    readHexMaybe :: String -> Maybe Int
    readHexMaybe s = case readHex s of
        [] -> Nothing
        (n, "") : [] -> Just n
    

    Now we get a warning saying not all cases are covered. Okay, what should happen if the s' position or otherResults position are not empty? Presumably Nothing again. So:

    readHexMaybe :: String -> Maybe Int
    readHexMaybe s = case readHex s of
        [] -> Nothing
        (n, "") : [] -> Just n
        (n, _) : _ -> Nothing
    

    In fact, it's simpler to put the successful case first and return Nothing in all other cases. We can also contract the syntax sugar of [x] for x:[].

    readHexMaybe :: String -> Maybe Int
    readHexMaybe s = case readHex s of
        [(n, "")] -> Just n
        _ -> Nothing
    

    Okay, we have a simpler type for reading now. If we want to gather up all the failures from a whole collection of such parses, we can use mapM. So:

    readAllHexMaybe :: [String] -> Maybe [Int]
    readAllHexMaybe = mapM readHexMaybe
    

    Finally, in main, we can turn Nothings into a message for the user, and Justs into a continuation that does something interesting with the list of [Int]s.

    main = do
        args <- getArgs
        fileName <- case args of
            [fileName] -> return fileName
            _ -> die "USAGE: ./whatever FILE"
        contents <- readFile fileName
        case readAllHexMaybe (words contents) of
            Nothing -> die "File contained things that didn't look like hex numbers"
            Just ns -> {- ... -}