Mark Needham

Thoughts on Software Development

Haskell: Processing program arguments

with one comment

My Prismatic news feed recently threw up an interesting tutorial titled ‘Haskell the Hard Way‘ which has an excellent and easy to understand section showing how to do IO in Haskell.

About half way down the page there’s an exercise to write a program which sums all its arguments which I thought I’d have a go at.

We need to use the System.getArgs function to get the arguments passed to the program. It has the following signature:

> :t getArgs
getArgs :: IO [String]

Using that inside a ‘do’ block means that we can get the list of arguments as a List of String values.

We then need to work out how to convert the list of strings into a list of integers so that we can add them together.

The way I’ve done that before is with the read function:

> map (\x -> read x :: Int) ["1", "2"]
[1,2]

That works fine as long as we assume that only numeric values will be passed as arguments but if not then we can end up with an exception:

> map (\x -> read x :: Int) ["1", "2", "blah"]
[1,2,*** Exception: Prelude.read: no parse

I wanted to try and avoid throwing an exception like that but instead add up any numbers which were provided and ignore everything else. The type signature of the function to process the inputs therefore needed to be:

[String] -> [Maybe Int]

With a bit of help from a Stack Overflow post I ended up with the following code:

import Data.Maybe
 
intify :: [String] -> [Maybe Int]
intify =  map maybeRead2
 
maybeRead2 :: String -> Maybe Int
maybeRead2 = fmap fst . listToMaybe . reads

reads has the following signature:

> :t reads
reads :: Read a => ReadS a

which initially threw me off as I had no idea what ‘ReadS’ was. It’s actually a synonym:

103
type ReadS a = String -> [(a,String)]

(defined in ./Text/ParserCombinators/ReadP.hs)

In our case I thought it’d do something like this:

> reads "1"
[(1, "1")]

But defining the following in a file:

a :: [(Int, String)]
a = reads "1"

suggests that the string version gets lost:

> a
[(1,"")]

I’m not sure I totally understand how that works!

Ether way, we then take the list of tuples and convert it into a Maybe using listToMaybe. So if we’d just parsed “1″ we might end up with this:

> listToMaybe [(1, "")]
Just (1,"")

I only have a basic understanding of fmap yet because I’m not up to that chapter in ‘Learn Me A Haskell‘.

As far as I know it’s used to apply a function to a value inside a container type object, i.e. the Maybe in this case, and then return the container type with its new value

In our case we start with ‘Just(1, “”)’ and we want to get to ‘Just 1′ so we use the ‘fst’ function to help us do that.

The main method of the program reads like this to wire it all together:

main = do args <- getArgs
          print (sum $ map (fromMaybe 0) $ (intify args))

I’m using fromMaybe to set a default value of 0 for any ‘Nothing’ values in the collection.

I compile the code like this:

> ghc --make learn_haskell_hard_way 
[1 of 1] Compiling Main             ( learn_haskell_hard_way.hs, learn_haskell_hard_way.o )
Linking learn_haskell_hard_way ...

And then run it:

>./learn_haskell_hard_way 1 2 3 4    
10
 
> ./learn_haskell_hard_way 1 2 3 4 mark says hello 5
15

Written by Mark Needham

April 8th, 2012 at 8:11 pm

Posted in Haskell

Tagged with

  • David Turner

    Hi Mark,

    The string that reads returns is whatever’s left after it parsed as much as it could, so ‘reads “1x” :: [(Int, String)] == [(1, "x")]‘. That’s why for a successful parse you are seeing an empty string, but beware that some illegal input (e.g. “1x”) will be handled by your program.

    Here map (fromMaybe 0) is just fine, but a handy function to know when you’re trying to turn a list of Maybes into a list of the actual values is catMaybes, which removes the Nothings and unwraps the Justs.

    Cheers,

    David