loading...
Cover image for Your first Stack project (build, test, dist)

Your first Stack project (build, test, dist)

egregors profile image Egregors Updated on ・8 min read

01 – VS Code for Haskell in 2020
02 – Your first Stack project (build, test, dist)

Stack – Haskell build tool-set

Despite conventional wisdom that Haskell is an academic-first language, there exist powerful production-ready build tools. The most common and useful for my opinion tool-set is Stack. Check previous post out to install stack and setup SV Code for development.

In this post, we'll try to implement a pretty simple project with Stack.

New Stack project

First of all, we need to init our awesome project using a new command: The stack new command will create a new directory containing all the needed files to start a project correctly.

Init from template

Stack new command creates a project skeleton according to some template. For our tiny project, we gonna use the default simple template:

stack new addStrInts

Now, you should see something like:

➜  addStrInts git:(master)ls
ChangeLog.md     README.md        addStrInts.cabal package.yaml     stack.yaml
LICENSE          Setup.hs         app              src              test

File structure

So, we got all the necessary files to start developing. Let's take a look at the most interesting stuff Stack created for us.

  • LICENSE – license according to Stack template (BSD3 by default)
  • addStrInts.cabal – project build config, we'll make a few changes here soon
  • app – folder with the application entry point
  • src – literally place for most of your code
  • test – folder for QuickCheck props test

The most important file here is addStrInts.cabal. This is the build configuration. Maintainer info, GHC version, dependencies, compiler options, etc. All of this could be found here.

For now, let's just update description, homepage, bug-reports and author, maintainer fields. Just put your GitHub user and Name into it.

Before we a going to add some code, let's check: is our project even compiles. For this purpose run stack setup and stack build (or, if you use VS Code, just press Stack Build button in the bottom). If you see the output like this — the project was built successfully

/Users/egregors/Documents/GitHub/addStrInts/addStrInts.cabal was modified manually. Ignoring /Users/egregors/Documents/GitHub/addStrInts/package.yaml in favor of the cabal file.
If you want to use the package.yaml file instead of the cabal file,
then please delete the cabal file.
Building all executables for `addStrInts' once. After a successful build of all of them, only specified executables will be rebuilt.
addStrInts> build (lib + exe)
Preprocessing library for addStrInts-0.1.0.0..
Building library for addStrInts-0.1.0.0..
[1 of 2] Compiling Lib
[2 of 2] Compiling Paths_addStrInts
Preprocessing executable 'addStrInts-exe' for addStrInts-0.1.0.0..
Building executable 'addStrInts-exe' for addStrInts-0.1.0.0..
[1 of 2] Compiling Main
[2 of 2] Compiling Paths_addStrInts
Linking .stack-work/dist/x86_64-osx/Cabal-2.4.0.1/build/addStrInts-exe/addStrInts-exe ...
addStrInts> copy/register
Installing library in /Users/egregors/Documents/GitHub/addStrInts/.stack-work/install/x86_64-osx/38556cea096b692240464d8e9f5f4a8fb0069d37b484d3266c298211dcc2a5e4/8.6.5/lib/x86_64-osx-ghc-8.6.5/addStrInts-0.1.0.0-2sRMucrO0w0Df0SlAhTLCI
Installing executable addStrInts-exe in /Users/egregors/Documents/GitHub/addStrInts/.stack-work/install/x86_64-osx/38556cea096b692240464d8e9f5f4a8fb0069d37b484d3266c298211dcc2a5e4/8.6.5/bin
Registering library for addStrInts-0.1.0.0..

Add Str Ints implementation

So, let's add some code! We wanna write a program which will be asking a few numbers from the user, and prints sum of them (yep, real-world Haskell app)

Let's start by making a few pure functions for typecast and calculate sum, next we'll add some tests, and finally, add one IO function for interaction with the user.

And Stack will help us with all this stuff.

Lib's code

All our "business" code we'll put in the StrAdd module, so let's create file StrAdd.hs in the src directory. You may delete Lib.hs, we don't need it anyway.

But now you should add the new dependency in our addStrInts.cabal file. Go to the library path, and replace Libs by AddStr.
To avoid build errors, edit app/Main.hs:

module Main where

main :: IO ()
main = undefined

Let's start with helpers. We will get strings as input, and we need to be sure all of the chars in the string are numbers.

module StrAdd where

import           Data.Char

isAllDigits :: String -> Bool
isAllDigits val = all (== True)
  $ map (\x -> x `elem` ['1', '2', '3', '4', '5', '6', '7', '8', '9']) val

To play around with our first function we should run the ghci session. Run stack ghci --ghci-options StrAdd.hs in terminal to load StrAdd.hs code and try to test isAllDigits function (or press Load GHCi if you use VS Code):

Loaded GHCi configuration from /private/var/folders/sk/jzr1583n0vs6zb05b0gb7hw40000gn/T/haskell-stack-ghci/9469312f/ghci-script
[1 of 1] Compiling StrAdd           ( StrAdd.hs, interpreted )
Ok, one module loaded.
*StrAdd> isAllDigits "1234"
True
*StrAdd> isAllDigits "4st"
False
*StrAdd> isAllDigits "123_5"
False

Looks good (no)! So, time to add some tests! Let's go into test/Spec.hs
Now this file contains stub:

main :: IO ()
main = putStrLn "Test suite not yet implemented"


in Spec.hs. To check test runner works properly, let's try run stack test.

You should see something like:

➜  src git:(master) ✗ stack test
/Users/egregors/Documents/GitHub/addStrInts/addStrInts.cabal was modified manually. Ignoring /Users/egregors/Documents/GitHub/addStrInts/package.yaml in favor of the cabal file.
If you want to use the package.yaml file instead of the cabal file,
then please delete the cabal file.
addStrInts-0.1.0.0: unregistering (local file changes: addStrInts.cabal)
addStrInts> configure (lib + exe + test)
Configuring addStrInts-0.1.0.0...
addStrInts> build (lib + exe + test)
Preprocessing library for addStrInts-0.1.0.0..
Building library for addStrInts-0.1.0.0..
Preprocessing test suite 'addStrInts-test' for addStrInts-0.1.0.0..
Building test suite 'addStrInts-test' for addStrInts-0.1.0.0..
Preprocessing executable 'addStrInts-exe' for addStrInts-0.1.0.0..
Building executable 'addStrInts-exe' for addStrInts-0.1.0.0..
addStrInts> copy/register
Installing library in /Users/egregors/Documents/GitHub/addStrInts/.stack-work/install/x86_64-osx/38556cea096b692240464d8e9f5f4a8fb0069d37b484d3266c298211dcc2a5e4/8.6.5/lib/x86_64-osx-ghc-8.6.5/addStrInts-0.1.0.0-2sRMucrO0w0Df0SlAhTLCI
Installing executable addStrInts-exe in /Users/egregors/Documents/GitHub/addStrInts/.stack-work/install/x86_64-osx/38556cea096b692240464d8e9f5f4a8fb0069d37b484d3266c298211dcc2a5e4/8.6.5/bin
Registering library for addStrInts-0.1.0.0..
addStrInts> test (suite: addStrInts-test)

Progress 1/2: addStrIntsTest suite not yet implemented

addStrInts> Test suite addStrInts-test passed
Completed 2 action(s). 

It’s time to add some tests.

Tests

First, let's try silly way. We'll add few test functions into Spec.hs:

import           StrAdd

main :: IO ()
main = do
  -- quickCheck prop_isAllDigit
  putStrLn ""
  putStrLn $ if isAllDigits "123" then "OK" else "FAIL!"
  putStrLn $ if not $ isAllDigits "4st" then "OK" else "FAIL!"
  putStrLn $ if not $ isAllDigits "123_4" then "OK" else "FAIL!"
  return ()

Now, just run stack test or press Stack Test button in the VS Code.

You should get smth like:

...

Progress 1/2: addStrInts
OK
OK
OK

addStrInts> Test suite addStrInts-test passed
...

Looks ok, but writing test this way is too boring and inefficient. And, obviously, it's hard to cover quite a few cases. That is why in Haskell we usually write property tests.

Let's take a look at the QuickCheck. The main idea is that you need to describe QuickCheck a set of the properties your function should have, and QuickCheck will automatically generate a bunch of testing data for this function.

Let's try to rewrite our test, but first, we need to add a new dependency. Go to addStrInts.cabal, test-suite addStrInts-test part and add QuickCheck to build-depends field:

...
test-suite addStrInts-test
  type: exitcode-stdio-1.0
  main-is: Spec.hs
  other-modules:
      Paths_addStrInts
  hs-source-dirs:
      test
  ghc-options: -threaded -rtsopts -with-rtsopts=-N
  build-depends:
      addStrInts
    , base >=4.7 && <5
    , QuickCheck
  default-language: Haskell2010

Now, we can use QuickCheck in our test. So, let's say if isAllDigit is true and it's not an empty string, then the length of the original string must be the equal string after isDigit filter:

import           StrAdd
import           Data.Char
import           Test.QuickCheck

prop_isAllDigit :: String -> Bool
prop_isAllDigit val = if isAllDigits val || val == ""
  then onlyDigit == length val
  else onlyDigit /= length val
  where onlyDigit = length $ filter isDigit val

main :: IO ()
main = do
  quickCheck prop_isAllDigit
  putStrLn "Done"

Now, run the test stack test:

➜  src git:(master) ✗ stack test
/Users/egregors/Documents/GitHub/addStrInts/addStrInts.cabal was modified manually. Ignoring /Users/egregors/Documents/GitHub/addStrInts/package.yaml in favor of the cabal file.
If you want to use the package.yaml file instead of the cabal file,
then please delete the cabal file.
addStrInts> test (suite: addStrInts-test)

*** Failed! Falsified (after 1 test):  
""
Done

addStrInts> Test suite addStrInts-test passed

And we got the first error! Looks like isAllDigit do not work properly for an empty string.

This is a quick fix (StrAdd.hs) by pattern matching:

module StrAdd where

import           Data.Char

isAllDigits :: String -> Bool
-- isAllDigits val = all (== True) $ map isDigit val
isAllDigits ""  = False
isAllDigits val = all (== True)
  $ map (\x -> x `elem` ['1', '2', '3', '4', '5', '6', '7', '8', '9']) val

So +++ OK, passed 100 tests. looks good, but 100 cases are too few for us. Let say, to be perfectly sure, we wanna 1000 cases:

main :: IO ()
main = do
  quickCheckWith stdArgs { maxSuccess = 1000 } prop_isAllDigit
  putStrLn "Done"

By quickCheckWith we ask QuickCheck to generate maxSuccess test cases. And dramatically fast we got another error:

*** Failed! Falsified (after 608 tests):                  
"0"
Done

It looks like, we forget about zero! I'm sure you noticed it right away.
Let's modify our function:

isAllDigits :: String -> Bool
isAllDigits ""  = False
isAllDigits val = all (== True) $ map isDigit val

Try to run stack test again to be sure, all works properly now. Well done!

Now just add the remaining code, and move on to writing the IO action.
Your StrAdd should look like:

module StrAdd where

import           Data.Char

isAllDigits :: String -> Bool
isAllDigits ""  = False
isAllDigits val = all (== True) $ map isDigit val

strAddInts :: String -> String -> Either String Int
strAddInts a b
  | isAllDigits a && isAllDigits b       = Right (read a + read b)
  | not (isAllDigits a || isAllDigits b) = Left "Both args are wrong"
  | not (isAllDigits a)                  = Left "First arg is wrong"
  | otherwise                            = Left "Second ars is wrong"

Entry point

Nobody likes dirty function, especially in Haskell, but unfortunately without any side effects our code becomes absolutely useless (even more useless then our current project!). So, let's add some IO actions for communication with the user.

Functions with side effects – the most dangerous and unpredictable functions in a whole project. So you need to try to make that code isolated. In this small Stack project, we'll place all user-interaction code in the program entry point app/Main.hs. This way, the entry point will be responsible just for interacting with the user, and all "business" code stays aside in src/StrAdd.hs.

module Main where

import           StrAdd

main :: IO ()
main = do
  putStrLn "Enter first argument"
  a <- getLine
  putStrLn "Enter second argument"
  b <- getLine

  let result = strAddInts a b

  putStrLn (displayResult result)

Don't forgate add displayResult helper function in StrAdd.hs:

displayResult :: Either String Int -> String
displayResult (Left  addError) = "error: " ++ show addError
displayResult (Right res     ) = "answer: " ++ show res

This is all. Now we should build the whole our project: stack build. If you did all right build process should finish without errors.

After the build, we finally may try to run our project as a real program: stack exec addStrInts-exe

Looks good!

➜  addStrInts git:(master) ✗ stack exec addStrInts-exe
Enter first argument
42
Enter second argument
69
answer: 111

Conclusion

According to the stack-way, you could develop Haskell applications within a full-featured powerful build system. Of cause, in this post, we touched just a tip of the iceberg, but it will be enough for the first meeting and start working with Stack.

We’ll meet a Stack much closer next time, trying to implement http interaction and external dependencies

Posted on by:

Discussion

markdown guide
 

On Mac, before stack setup, I had to do
sudo xcode-select --switch /Library/Developer/CommandLineTools to make it work