Egregors
Posted on January 30, 2020
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 January 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.