Beginning Haskell the practical way

I found many useful tutorials for Haskell which do a great job explaining the language details and the mathematical nature of it, however, almost none gets you hacking in a few moments so you can experience Haskells power in the real world. But since using a new language is at least in the beginning the probably fastest way to learn it, this doesn't seem to really help a beginner, so I want to share my experiences with Haskell, so you can start right away writing your first simple tools.

Let's start right from the beginning: how to run your Haskell programms. Make sure you have the Glasgow Haskell Compiler installed and your $PATH is correctly set. First write the obligatory hello world program, so fire up your editor and create a File called HelloWorld.hs:

main = do
    putStrLn "Hello, world"

Haskell can be run either interpreted or compiled. So try first running hello world as a script: runghc HelloWorld.hs. It should print "Hello, world" to your terminal and exit. However, for larger programs it might be convenient to compile it, so to compile a simple program just type in ghc HelloWorld.hs -o HelloWorld. A few seconds after that there should be an executable named "HelloWorld" in your directory, which also prints the famous hello world string when executed. Haskell supports putting a shebang in your source code, so if you start HelloWorld.hs with the line #!/usr/bin/runghc (or similar if your path differs) and you allow execution of the source file, you can run it via ./HelloWorld.hs like you normally would with Python- or Bash-scripts. In addition to run programs, Haskell provides just like python or ruby an interactive shell, invocable via ghci; this can be handy at some times, especially the :type command: it shows you the type of an object or function, so if your compiler mumbles something about inferred and expected types, you should :type the functions troubling you. Let's take a brief look at it and inspect our HelloWorld.hs in a ghci-session

Prelude> :type putStrLn
putStrLn :: String -> IO ()
Prelude> :type "Hello, world"
"Hello, world" :: [Char]

Quite harmless, hu? putStrLn has the type String -> IO (), which basically means, it takes a String as argument and "returns" an IO ()-action. Here lies some of the beauty of the language, because it strictly differentiates between so called pure and impure functions: pure functions never have side effects, so you can easily prove their correctness (and Haskell can optimize the shit out of them), while impure functions are the places where you make your hands dirty, interact with the environment and where everything can blow up. Haskell encapsulates these impure actions in a construct called monads, so if you want to learn more about them, there are great explainations online, I can especially recommend Real World Haskell. But we just want to get some basic scripts working at the moment.

So, while most tutorials focus on explaining the language and take a long while until you finally reach the place where you can interact with your program, I'll show you how to simply interact with your system and explain the language along the way. One of the most basic ways to interact are program arguments, so let's write a program that PrintArguments.hs prints the supplied arguments:

import System.Environment (getArgs)

main = do
    args <- getArgs
    print args

If you invoke this program without supplying arguments, you'd maybe expect it to print out the programs name; however in contrast to most programming languages, getArgs only gives you the arguments, not the program name. If you invoke it with some arguments, you'll see a list of those arguments. Let's check if it really is a list in ghci:

Prelude> import System.Environment
Prelude System.Environment> args <- getArgs
Prelude System.Environment> :type args
args :: [String]

So it really gives you a list of String. You might have tried import System.Environment (getArgs) in ghci, but this gives an error; this feature is only available in scripts or programs, to reduce the code you load on the import. Now we want to print this string without the help of print, which uses the show function for the object to get it's string representation. args holds a list of the arguments, so we need to make a string from it, similar to ' '.join(list_of_strings) in python. One could loop over the list and print each argument, but we want to concatenate these to one line. For this we use the intercalate function provided by the Data.List module, which holds lots of useful functions and name the program Echo.hs, since it works almost like the echo command on your shell:

import System.Environment (getArgs)
import Data.List

main = do
    args <- getArgs
    putStrLn $ intercalate " " args

So running runghc Echo.hs arg1 arg2 arg3 will print arg1 arg2 arg3 on your command line, just as expected. You might wonder about the $-sign: this is just some really nice syntactical sugar to reduce the number of parentheses, because it wraps everything that follows on the line into parentheses. In this case putStrLn $ intercalate " " args is equivalent to putStrLn (intercalate " " args). We can now read arguments from the command line, and to process them we could use any other function instead of putStrLn that takes a list of strings or, after the use of intercalate, a single string. But if we want to supply numerical arguments, things get much more complicated. For illustration, let us write a program Add.hs that takes a list of numbers and adds them up. Here we need to use the readFloat function from the Numeric module, so let's first try it on the ghci:

Prelude> import Numeric
Prelude Numeric> :type readFloat
readFloat :: (RealFrac a) => String -> [(a, String)]
Prelude Numeric> --the type signature doesn't look helpful, let's just try it:
Prelude Numeric> readFloat "3.141592653"
[(3.141592653,"")]
Prelude Numeric> readFloat "3.141592653 2.99 13"
[(3.141592653," 2.99 13")]
Prelude Numeric> readFloat "3.141 not numbers"
[(3.141," not numbers")]
Prelude Numeric> readFloat "not a number"
[]

What readFloat does is this: it takes a string and returns a list, which is empty, if the string does not begin with a number, or contains a tuple consisting of the first number and the rest of the string. We can now either join our arguments as above and try to write a recursive function using readFloat, or we write a function that parses one number and map on our argument list. In our program it makes sense to assume that an argument, which is not a number won't be added up, so we assume its value is zero. First we define the function to convert a string to a number in our Add.hs.

import System.Environment (getArgs)
import Numeric

argumentToFloat :: String -> Float
argumentToFloat = extractFromTuple . readFloat
    where
        extractFromTuple [] = 0
        extractFromTuple x = fst $ head x

main = do
    args <- getArgs
    putStrLn $ show $ sum $ map argumentToFloat args

Here we first defined the type of argumentToFloat: it should take a string and return a float, no matter what. For the definition we use the . operator, which is the same as composition in mathematics and is similar to $, only that in this case we don't provide an argument. Because readFloat returns such a strange list of tuple or an empty list, we use a function extractFromTuple, which doesn't exist yet. But to keep the code concise, Haskell allows us to define local functions and variables with the where keywords. There we use something that's called pattern matching in Haskell, which is similar to function overloading in other languages, but much more powerful. We first define extractFromTuple [] = 0, which says if the function is invoked with an empty list, it should return 0, since it makes sense in this context. However, if the list is not empty, extractFromTuple x should first take the head of the list x, then extract the first element from the tuple; thus we use fst $ head x for it's definition. Our main function is pretty concise: we map our newly defined function over the argument list, use the built-in function sum to add up all numbers, convert the result to its string representation via show and then print it via putStrLn.

But how can we react in a more sophisticated way, if a supplied argument is not a number? One of Haskell's several ways to do this is the Maybe-monad: remember that monads are where we encapsulate things that can blow up. Let's write a program PowerOfTwo.hs, that prints the power of two of a supplied arguuent if it's a number or an error message.

import System.Environment (getArgs)
import Numeric
import System.IO
import Control.Monad

safeReadFloat :: String -> Maybe Float
safeReadFloat = safeExtract . readFloat
    where
        safeExtract :: [(Float, String)] -> Maybe Float
        safeExtract list =
            case list of
                [(x,"")]    -> Just x
                _           -> Nothing

printPowerOfTwo :: Maybe Float -> IO ()
printPowerOfTwo x =
    case x of
        Just val    -> putStrLn $ show $ val*val
        Nothing     -> hPutStrLn stderr "You have to supply exactly one numerical argument"


main = do
    args <- getArgs
    case args of
        [x]    -> printPowerOfTwo $ safeReadFloat x
        _      -> hPutStrLn stderr "You have to supply exactly one argument"

Here we use Haskells powerful pattern matching again several times with the case keyword, which is similar to switch case constructs in other languages. We import System.IO for hPutStrLn, which takes a file handle and a string, to put the string to the supplied file handle; in this case stderr. Further we import Control.Monad for the Maybe-monad, which we use in safeReadFloat: if we can read a float, and only a float, we return Just x, which else we return Nothing. By doing this we can pattern match again in printPowerOfTwo, and either print the quadratic value, or an error message. By doing this we encapsulated a possible error in the value Nothing. We could have also structured the program in another way and return a negative number, if the argument is not numeric, and the quadratic number else, however often your function is possibly surjective, so you can't reserve error codes, as one often does in C.

This should be enough to get started with simple Haskell programs, since you can read and evaluate your command line arguments. Since this post grew a bit larger than expected, I'll postpone file interaction for now, because Haskell makes some powerful simplifications and optimizations which can cause trouble for the beginner.

blogroll

social