Haskell Tutorial

2208 words

v1

Originally published on eighttrigrams.net on April 12th, 2023

This is the minimalist’s missing Haskell tutorial, intended to bridge the gap between
language-focused tutorials and setting up a project.

Getting started with the REPL

To get started as quick as possible, let’s fire up the Glasgow Haskell Compiler’s (GHC) Read Eval Print Loop (REPL), the GHCi, where the ‘i’ stands for interpreter. Feed it code and it will be interpreted, not compiled. It will be slower, but you get quick feedback during development.

$ ghci
GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
Prelude> 2 + 2
4

Inside the REPL, one can define and use functions

Prelude> times2 x = x + x
Prelude> times2 2
4

This quickly becomes awkward with longer function bodies. In principle, one can
use multiline definitions

Prelude> :{
Prelude| times2 x =
Prelude|    x + x
Prelude| :}
Prelude> times2 2
4

but this usually isn’t super practical either. If there is a typo, one cannot easily re-create
the whole thing. Pressing the Up-Arrow key gives us the previously entered commands from the command history.
But here it gives us one line at a time, so we would need to put the pieces back together one by one.

So the obvious thing is to define programs in files.

Writing programs

We can put some code in a file and run with the interpreter.

examples/haskell/hello.hs:

main = do
    print "Hello, World!"

If the runhaskell is installed on your machine, you can run it with

examples/haskell$ runhaskell hello.hs
"Hello, World!"

Note: Haskell files should be formatted using spaces for the indentation. Also note that the indentation carries meaning, that is, the compiler will complain and not compile incorrectly formatted files.

Now we run this code from within the interpreter

examples/haskell$ ghci hello.hs 
GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( hello.hs, interpreted )
Ok, one module loaded.
*Main> main
"Hello, World!"

To reload after changes have been made (remove the ! for example) use :r.

*Main> :r
[1 of 1] Compiling Main             ( hello.hs, interpreted )
Ok, one module loaded.
*Main> main
"Hello, World"

You can define functions

examples/haskell/functions.hs:

times2 n = n + n

main = do
    print (times2 2)

That means one can develop can call different parts indepently inside the interpeter

examples/haskell$ ghci functions.hs 
GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( functions.hs, interpreted )
Ok, one module loaded.
*Main> main
4
*Main> times2 2
4

Now our problem from earlier is solved. We can confortably use a text editor to edit our
functions instead of doing that on the REPL, but we can trigger them from the REPL and see their effects.

Modules

Programs are distributed into modules, which sit in different files.

examples/haskell/modules1/MyModule.hs:

module MyModule where

times2 n = n + n

examples/haskell/modules1/go.hs:

import MyModule

main = do 
   print (times2 2)

Run this with

examples/haskell$ cd modules1
examples/haskell/modules1$ ghci go.hs 
GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( go.hs, interpreted )
Ok, one module loaded.
*Main> main
4
*Main> times2 2
4

Note that :r after making changes to any module leads to the entire thing to be re-interpreted.
So a new call to main should reflect any changes.

Note also that since MyModule is imported into the Main module, we can call times2 from within the
*Main module. Actually what the *Main> prompt tells us is that we operate inside the context of the Main module.
Contrast that with when you open the interpreter without arguments ($ ghci). This will show you the Prelude> prompt,
signalling we operate withing the context of the standard library (Prelude).

If (and only if) you make a change to MyModule.hs and afterwards call :r MyModule, the prompt will change to *MyModule>, so we operate in the context of the module MyModule and will be able only to access things defined there. Entering :r will bring
us back.

Submodules

With more code the next question to address is how to organise it hierarchically.
The following should serve as an example.

examples/haskell/submodules/MyModule/MySubModule.hs:

module MyModule.MySubModule where

times3 x = x + x + x

examples/haskell/submodules/MyModule.hs:

module MyModule where

import MyModule.MySubModule

times2 x = x + x

times6 x = times3 x + times3 x

examples/haskell/submodules/main.hs

import MyModule
import MyModule.MySubModule

main = do 
   print (times2 2)
   print (times3 3)
   print (times6 6)

Run it

examples/haskell/submodules$ runhaskell main.hs 
4
9
36

Qualified imports

When importing modules, both the module’s own and the imported modules’
symbols become available, which can lead to clashes. One means of avoiding
it is qualifying imports, such that imported functions are called with a prefix of choice.

We assume the same folder layout as in the previous example
and change only main.hs.

examples/haskell/qualified_imports/main.hs:

import MyModule as MM
import MyModule.MySubModule as MSM

main = do 
   print (MM.times2 2)
   print (MSM.times3 3)
   print (MM.times6 6)

In the REPL

$ ghci main
...
*Main> main
4
9
36
*Main> MM.times2 2
4

One also could use M.MySubModule, that is, something including dots, as the shorthand.

Making a Main module

Given

examples/haskell/main_module/MyModule.hs

module MyModule where

times2 x = x + x

when we create a Main module

examples/haskell/main_module/Main.hs

module Main where

import MyModule as MM

main = do 
   print (MM.times2 2)

instead of giving main.hs as parameter, we use Main, like this

examples/haskell/main_module$ runhaskell Main

or like this

examples/haskell/main_module$ ghci Main

Using available modules

A lot of modules come already with a Haskell installation. I am not sure
which ones these are, but Data.Char is amongst them and should serve as our example here.

examples/haskell/external_modules/main.hs

import Data.Char

main = do 
   print $ show $ isLower 'a'

Run it

examples/haskell/external_modules$ runhaskell main.hs 
"True"

Installing external modules with Cabal

Modules come bundled in packages and are available via the Hackage package registry.
I am sure they can be downloaded and linked with more low level tools, but nowadays those things are handled by a package manager,
in our case Cabal.

Let us try out downloading a package and make use of a new module. We start by updating the package info with

$ cabal update

Then we download the package ABList

$ cabal install --lib ABList

This installs and makes the package globally available. When you fire up ghci, you should see something like this

$ ghci                                     
GHCi, version 8.10.5: https://www.haskell.org/ghc/  :? for help
Loaded package environment from /<userdir>/.ghc/x86_64-darwin-8.10.5/environments/default
Prelude> 

We can list available symbols with

Prelude> :browse Data.AbList
type Data.AbList.AbList :: * -> * -> *
data Data.AbList.AbList a b
  = Data.AbList.AbNil | a Data.AbList.:/ (Data.AbList.AbList b a)
Data.AbList.aaFromList :: [a] -> Data.AbList.AbList a a
Data.AbList.aaMap ::
  (a -> b) -> Data.AbList.AbList a a -> Data.AbList.AbList b b
Data.AbList.aaToList :: Data.AbList.AbList a a -> [a]
Data.AbList.abFoldl ::
  (t -> Either a b -> t) -> t -> Data.AbList.AbList a b -> t
...

and use them

Prelude> import Data.AbList
Prelude Data.AbList> abFromPairs [(1,"3"),(3,"7")]
1 :/ ("3" :/ (3 :/ ("7" :/ AbNil)))

Don’t ask :D. I have no idea what it does.

Anyways, like you did earlier with import Data.Char, in every program (i.e. not only in ghci) you can
import modules from the ABList package.

Looking at the output of cat ~/.ghc/x86_64-darwin-8.10.5/environments/default (see path given above when running ghci),
we should see the ABList package listed.

...
package-id ghc-8.10.5
package-id bytestring-0.10.12.0
package-id unix-2.7.2.2
package-id base-4.14.2.0
package-id time-1.9.3
...
package-id text-1.2.4.1
package-id ABLst-0.0.3-261a7b41     <--------------- here

Note in case at some point you run into trouble with unsatisfiable dependencies, i.e. when you cabal install out
put includes something like this

Resolving dependencies...
cabal: Could not resolve dependencies:
rejecting: ...

the easy (brute force) way out is

$ rm -rf ~/.ghc ~/.cabal

This resets all packages. Next time you want to install something, chances are good that it works, but it affects all your
other projects. All dependencies need to be downloaded anew. Make sure to follow this up with a cabal update, otherwise it will complain
that it cannot find packages.

Concluding this secontion, we should say that instead of installing packages globally,
it is probably better to set up projects with Cabal and declaring package dependencies ‘locally’.

Using external modules with Cabal projects

Cabal is not only a package-, but also a build-manager, which can help building and packaging more complex projects.
Let’s set up an example project using it.

examples/haskell$ mkdir cmodules && cd cmodules # already done in
examples/haskell/cmodules$ cabal init           # example project

We also want to demonstrate how to use external dependencies. For that, add a dependency to
the package split by editing

examples/haskell/cmodules/cmodules.cabal

...
executable cmodules
...
  build-depends:       base >=4.12 && <=4.18.0
                    , split >=0.2.3.5 && <=0.2.3.5
...

examples/haskell/cmodules/Main.hs:

module Main where

import Data.List
import Data.List.Split

main :: IO ()
main = putStrLn $ concat (splitOn "x" "axbxc")

Run

examples/haskell/cmodules$ cabal run cmodules
Resolving dependencies...
... lots of text
Building executable 'cmodules' for cmodules-0.1.0.0..
... more text
Linking ...
abc

Testing with HUnit

Now let’s set up some unit tests.

$ mkdir hunit_testing
$ cd hunit_testing
$ cabal init
$ rm -r app

<pre hidden> TODO Actually it would be nicer to not repeat the info in the above block since we already explained that </pre>

examples/haskell/hunit_testing/hunit-testing.cabal:

...
test-suite my-test-suite
  type:                exitcode-stdio-1.0
  main-is:             MyTestSuite.hs
  build-depends:       base >=4.13 && <4.18
                     , HUnit
  default-language:    Haskell2010
...

examples/haskell/hunit_testing/MyTestSuite.hs:

module Main where

import Test.HUnit

test1 = TestCase $ assertEqual "a succeeding test" "a" "a"

test2 = TestCase $ assertEqual "a failing test" "a" "b"

main = 
    runTestTT $ TestList [test1, test2]

Note that we need to name the module Main despite it having a different file name.

We can run it either with

examples/haskell/hunit_testing$ cabal run my-test-suite
...
### Failure in: 1                         
MyTestSuite.hs:7
a failing test
expected: "a"
 but got: "b"
Cases: 2  Tried: 2  Errors: 0  Failures: 1

or we can use

examples/haskell/hunit_testing$ cabal test my-test-suite
...
Test suite my-test-suite: RUNNING...
Test suite my-test-suite: PASS
Test suite logged to:
<...>my-test-suite/test/hunit-testing-0.1.0.0-my-test-suite.log
1 of 1 test suites (1 of 1 test cases) passed.

the difference being that in the latter case we need to look up the test results in the log file

$ cat <...>my-test-suite/test/hunit-testing-0.1.0.0-my-test-suite.log
...
Test suite my-test-suite: RUNNING...
### Failure in: 1                         
MyTestSuite.hs:7
a failing test
expected: "a"
 but got: "b"
Cases: 2  Tried: 2  Errors: 0  Failures: 1
Test suite my-test-suite: PASS
...

I’m not sure why this is considered PASSed, but whatever 🤷; at least it correctly counts 1 Failure.

Separate src and test folders

examples/haskell/separate_src_and_test/separate-src-and-test.cabal:

...
executable main
  main-is:             Main.hs
  hs-source-dirs:      src
  build-depends:       base >=4.13 && <=4.18
  default-language:    Haskell2010
  other-modules:       MyModule

test-suite my-test-suite
  type:                exitcode-stdio-1.0
  main-is:             MyTestSuite.hs
  hs-source-dirs:      src, test
  build-depends:       base >=4.13 && <=4.18
                     , HUnit
  default-language:    Haskell2010
  other-modules:       MyModule
...

examples/haskell/separate_src_and_test/src/Main.hs:

module Main where

import MyModule

main =
    print $ times2 2

examples/haskell/separate_src_and_test/src/MyModule.hs:

module MyModule where

times2 x = x + x

examples/haskell/separate_src_and_test/test/MyTestSuite.hs:

module Main where

import Test.HUnit
import MyModule

test1 = TestCase $ assertEqual "a failing test" 5 (times2 2)

main = 
    runTestTT $ TestList [test1]

Run test

examples/haskell/separate_src_and_test$ cabal run my-test-suite
...
### Failure in: 0                         
test/MyTestSuite.hs:6
a failing test
expected: 5
 but got: 4
Cases: 1  Tried: 1  Errors: 0  Failures: 1

Debugging

Many people find is perfectly fine to do the good old printf-debugging.
To do that in haskell, we can make use of the Debug.Trace module, which ships
with the base package.

It comes with a couple of handy functions, of of which is trace. With trace you
can do something like this:

examples/haskell/debugging/dbg.hs:

import Debug.Trace

debug msg d = trace (msg ++ show d) d

times2 = (*) 2

main = 
    putStrLn 
    $ show 
    $ debug "2: "
    $ times2
    $ debug "1: " 
    $ times2 
    $ 3

We wrap trace in a convenient helper function which can be inserted into a flow.

Run it

examples/haskell/debugging$ runhaskell dbg.hs 
1: 6
2: 12
12

It prints intermediate values but otherwise proceeds
to transform values as if the debug calls weren’t there.
You can conveniently comment out and comment in the debug lines during debugging.

One more example shows how we can define debug slightly differently to fit another use case:

examples/haskell/debugging/dbg2.hs:

import Debug.Trace

debug d msg = trace (msg ++ show d) d

isPos n
  | n<0       = False `debug` "f: "
  | otherwise = True `debug` "t: "

main = 
    putStrLn 
    $ show 
    $ isPos 3

Run it

examples/haskell/debugging$ runhaskell dbg2.hs
t: True
True

Defining it thus allows us again to comment out the debug calls.

isPos n
  | n<0       = False -- `debug` "f: "
  | otherwise = True -- `debug` "t: "

Final words

If you liked this and want to suggest an improvement,
feel free to shoot me a mail.

# Comments

Leave a comment