Haskell, named after logician Haskell Curry, is a standardised, general-purpose, purely functional programming language, with non-strict semantics and strong static typing. Lets take a look at the property-based testing of Haskell programs and at the Cabal tool, which is used to build and manage Haskell packages and applications
One of the main features of testing in Haskell is property-based testing. The type system allows you to infer and derive types, and also helps in auto-generating test cases. QuickCheck is a popular property-based testing library for Haskell. If your program is pure, you can write tests to ascertain the properties and invariants of your programs, and the tests can be auto-generated and executed.
You can install QuickCheck on Fedora, for example, by using the following command:
$ sudo yum install ghc-QuickCheck-devel
Consider a simple function to add two integers:
mySum :: Int -> Int -> Int mySum a b = a + b
You can ascertain the property of the function that a + b is the same as b + a by using the QuickCheck library. You must first define the invariant in a function as shown below:
prop_mySum a b = mySum a b == mySum b a
You can test the code directly in the GHCi prompt, using the following command:
$ ghci sum.hs GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. [1 of 1] Compiling Main (sum.hs, interpreted) Ok, modules loaded: Main. ghci> prop_mySum 2 3 Loading package array-0.4.0.1 ... linking ... done. Loading package deepseq-1.3.0.1 ... linking ... done. Loading package old-locale-1.0.0.5 ... linking ... done. Loading package time-1.4.0.1 ... linking ... done. Loading package random-1.0.1.1 ... linking ... done. Loading package containers-0.5.0.0 ... linking ... done. Loading package pretty-1.1.1.0 ... linking ... done. Loading package template-haskell ... linking ... done. Loading package QuickCheck-2.6 ... linking ... done. True
You can also invoke the quickCheck function in a main function, as shown below:
import Test.QuickCheck mySum :: Int -> Int -> Int mySum a b = a + b prop_mySum :: Int -> Int -> Bool prop_mySum a b = mySum a b == mySum b a main :: IO () main = quickCheck prop_mySum
Compiling and executing the above code produces the following output:
$ ghc --make sum.hs [1 of 1] Compiling Main (sum.hs, sum.o) Linking sum ... $ ./sum +++ OK, passed 100 tests.
You can also dump the input that was generated for the various test cases using the verboseCheck function, as shown below:
main :: IO () main = verboseCheck prop_mySum
Executing the above code with the updated main function will yield 100 input test cases that were generated in runtime.
ghci> main Passed: 0 0 Passed: -1 1 Passed: 64 -44 Passed: -2159 2134 Passed: -927480859 61832343 ...
The head function in Haskell expects to receive a non-empty list. You can write a headExists function to check if the head exists for a list of integers, as shown below:
headExists :: [Int] -> Bool headExists list | null list = False | otherwise = True
You can load the above code in GHCi and test it out, as follows:
ghci> headExists [] False ghci> headExists [1, 2, 3] True
Lets assume that, by mistake, you wrote an incorrect property-based test where the headExists function will always return False, ignoring the otherwise case.
import Test.QuickCheck headExists :: [Int] -> Bool headExists list | null list = False | otherwise = True prop_headExists :: [Int] -> Bool prop_headExists emptyList = headExists emptyList == False main :: IO () main = quickCheck prop_headExists
Testing the code produces the following output:
$ ghc --make head.hs [1 of 1] Compiling Main (head.hs, head.o) Linking head ... $ ./head *** Failed! Falsifiable (after 3 tests): [0]
The QuickCheck library generated test cases for different [Int] types and it returned a failure after the third test, for which the input was [0]. Clearly, the headExists [0] computation will return True and not False.
The way we defined the property is incorrect. We know that if the list is empty, then its length is zero. We can write a helper function lengthZero for the above, as follows:
lengthZero :: [Int] -> Bool lengthZero list | length list == 0 = True | otherwise = False
We can then use this function to assert that for any Integer list, if headExists returns False then the lengthZero function must return True. The complete code is shown below:
import Data.List import Test.QuickCheck headExists :: [Int] -> Bool headExists list | null list = False | otherwise = True lengthZero :: [Int] -> Bool lengthZero list | length list == 0 = True | otherwise = False prop_headExists :: [Int] -> Bool prop_headExists list = headExists list == not (lengthZero list) main :: IO () main = quickCheck prop_headExists
Executing the code produces the required output:
$ ghc --make head.hs [1 of 1] Compiling Main (head.hs, head.o ) Linking head ... $ ./head +++ OK, passed 100 tests.
We can also re-write the above code based on conditional properties. The property that the headExists function will return True only for non-empty lists can be defined as a constraint. The notation syntax is condition ==> property. In our example, if the condition that the list is non-empty is True, then the property that the headExists function for the list must return is True. Also, when the list is empty, the headExists function must return False. These two conditions can be written as follows:
import Data. List import Test.QuickCheck headExists :: [Int] -> Bool headExists list | null list = False | otherwise = True prop_headExists :: [Int] -> Property prop_headExists list = length list > 0 ==> headExists list == True prop_emptyList :: [Int] -> Property prop_emptyList list = length list == 0 ==> headExists list == False main :: IO () main = do quickCheck prop_headExists quickCheck prop_emptyList
Testing the code produces the following output:
$ ghc --make cond.hs [1 of 1] Compiling Main (cond.hs, cond.o ) Linking cond ... $ ./cond +++ OK, passed 100 tests. *** Gave up! Passed only 38 tests.
These tests can be integrated with Hspec or HUnit for a more verbose output.
Cabal
Cabal is a software tool that is used to describe a Haskell application, list its dependencies, and provide a manifestation to distribute the source and binaries. It is not to be confused with a distribution package manager like RPM or the Debian package management system. You can install Cabal using your distribution package manager. On Fedora, for example, you can use the following command:
$ sudo yum install cabal-install
Haskell software programs are available at hackage.haskell.org, and each project has a .cabal file. Let us look at the example of the HSH-2.1.2 package at http://hackage.haskell.org/package/HSH which allows you to use shell commands and expressions within Haskell programs. You can download the HSH-2.1.2.tar.gz and extract it using:
$ tar xzvf HSH-2.1.2.tar.gz HSH-2.1.2/ HSH-2.1.2/COPYING HSH-2.1.2/HSH.cabal HSH-2.1.2/testsrc/ HSH-2.1.2/testsrc/runtests.hs HSH-2.1.2/HSH.hs HSH-2.1.2/HSH/ HSH-2.1.2/HSH/Command.hs HSH-2.1.2/HSH/ShellEquivs.hs HSH-2.1.2/HSH/Channel.hs HSH-2.1.2/COPYRIGHT HSH-2.1.2/Setup.lhs
The .cabal file has various fields that describe the Haskell application. The contents of the HSH.cabal for version 2.1.2 are given below:
Name: HSH Version: 2.1.3 License: LGPL Maintainer: John Goerzen <jgoerzen@complete.org> Author: John Goerzen Stability: Beta Copyright: Copyright (c) 2006-2014 John Goerzen Category: system license-file: COPYRIGHT extra-source-files: COPYING homepage: http://software.complete.org/hsh Synopsis: Library to mix shell scripting with Haskell programs Description: HSH is designed to let you mix and match shell expressions with Haskell programs. With HSH, it is possible to easily run shell commands, capture their output or provide their input, and pipe them to and from other shell commands and arbitrary Haskell functions at will. Category: System Cabal-Version: >=1.2.3 Build-type: Simple flag buildtests description: Build the executable to run unit tests default: False library Exposed-Modules: HSH, HSH.Command, HSH.ShellEquivs, HSH.Channel Extensions: ExistentialQuantification, OverlappingInstances, UndecidableInstances, FlexibleContexts, CPP Build-Depends: base >= 4 && < 5, mtl, process, regex-compat, MissingH>=1.0.0, hslogger, filepath, regex-base, regex-posix, directory, bytestring if !os(windows) Build-Depends: unix GHC-Options: -O2 -threaded -Wall Executable runtests if flag(buildtests) Buildable: True Build-Depends: base >= 4 && < 5, mtl, process, regex-compat, MissingH>=1.0.0, hslogger, filepath, regex-base, regex-posix, directory, bytestring, HUnit, testpack if !os(windows) Build-Depends: unix else Buildable: False Main-Is: runtests.hs HS-Source-Dirs: testsrc, . Extensions: ExistentialQuantification, OverlappingInstances, UndecidableInstances, FlexibleContexts, CPP GHC-Options: -O2 -threaded
Enter the HSH-2.1.2 directory and configure the project using the cabal configure command as shown below:
$ cd HSH-2.1.2 $ cabal configure Resolving dependencies... Configuring HSH-2.1.2...
You can then compile the project sources using the cabal build step:
$ cabal build Building HSH-2.1.2... Preprocessing library HSH-2.1.2... [1 of 4] Compiling HSH.Channel (HSH/Channel.hs, dist/build/HSH/Channel.o ) ... [2 of 4] Compiling HSH.Command (HSH/Command.hs, dist/build/HSH/Command.o ) ... [3 of 4] Compiling HSH.ShellEquivs (HSH/ShellEquivs.hs, dist/build/HSH/ShellEquivs.o ) ... [4 of 4] Compiling HSH (HSH.hs, dist/build/HSH.o ) In-place registering HSH-2.1.2...
You can install the built library files using the cabal install command. By default, it installs to the ~/.cabal folder as shown below:
$ cabal install Resolving dependencies... Configuring HSH-2.1.2... Building HSH-2.1.2... Preprocessing library HSH-2.1.2... In-place registering HSH-2.1.2... Installing library in /home/guest/.cabal/lib/HSH-2.1.2/ghc-7.6.3 Registering HSH-2.1.2... Installed HSH-2.1.2
You can also generate HTML documentation for the source code using the cabal haddock option. The HTML files can also be made available at hackage.haskell.org:
$ cabal haddock Running Haddock for HSH-2.1.2... Preprocessing library HSH-2.1.2... Warning: The documentation for the following packages are not installed. No links will be generated to these packages: MissingH-1.3.0.1, rts-1.0, hslogger-1.2.6, network-2.6.0.2 Haddock coverage: ... Documentation created: dist/doc/html/HSH/index.html
If you make changes to the sources and wish to generate a new release, you can update the Version field in the HSH.cabal file:
Version: 2.1.3
In order to make a new tarball, use the cabal sdist command:
$ cabal sdist Distribution quality warnings: ... Building source dist for HSH-2.1.3... Preprocessing library HSH-2.1.3... Source tarball created: dist/HSH-2.1.3.tar.gz
To test the installed application, you can run GHCi from a directory other than the HSH-2.1.2 sources directory. For example:
$ ghci GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. ghci> :m + HSH ghci HSH> runIO date Sun Jan 4 14:22:37 IST 2015
You should not run GHCi from the sources directory, since it will find the module in it and try to use it instead of the installed modules in the ~/.cabal folder.
You can also test the installation by writing a program:
import HSH.Command main :: IO () main = do runIO date
You can compile and execute the above as shown below:
$ ghc --make test.hs [1 of 1] Compiling Main ( test.hs, test.o ) Linking test ... $ ./test Sun Jan 4 14:25:19 IST 2015
You are encouraged to read the Cabal guide at
https://www.haskell.org/cabal/ for information on specific fields and their options.