It is a collection of many tips and tricks for Haskell development.
Configuration files can be written in Haskell since it provides a parser (readMaybe) that can read arbitrary data structures defined as instance of show and read type classes.
Example:
File: serverConf.hs
import Text.Read (readMaybe)
import Text.Printf (printf)
type Password = String
type Username = String
data ServerAuth = AuthEmpty
| AuthBasic Username Password
deriving (Eq, Read, Show)
data ServerConf = ServerConf
{
serverHost :: String
,serverPort :: Int
,serverAssets :: String
,serverSecurity :: ServerAuth
} deriving (Eq, Read, Show)
readConfig1 :: String -> Maybe ServerConf
readConfig1 = readMaybe
readConfig2 :: String -> Maybe ServerConf
readConfig2 str = readMaybe str
readConfigFile :: String -> IO (Maybe ServerConf)
readConfigFile configFile = readConfig1 <$> readFile configFile -- Or fmap readConfig1 (readFile configFile)
runServer :: ServerConf -> IO ()
runServer config = do printf "Running server at port %d, listening host %s\n" (serverPort config) (serverHost config)
putStrLn $ "Server Authentication = " ++ show (serverSecurity config)
runServerConfigFile :: String -> IO ()
runServerConfigFile configFile = do
config <- readConfigFile configFile
case config of
Just conf -> runServer conf
Nothing -> putStrLn "Error: I can't read the server configuration file"
config1 = ServerConf "0.0.0.0" 8080 "/var/www" AuthEmpty
config2 = ServerConf { serverHost = "0.0.0.0",
serverPort = 9090,
serverAssets = "/home/arch/www",
serverSecurity = AuthBasic "gh0st" "xmkg3p98sfawgav"
}
Configuration File: server.conf
ServerConf {
serverHost = "127.0.0.1"
,serverPort = 80
,serverAssets = "/var/www"
,serverSecurity = AuthBasic "admin" "xjwxfm2pcfg56hga"
}
Load the file in the REPL:
> :load /tmp/serverconf.hs
[1 of 1] Compiling Main ( /tmp/serverconf.hs, interpreted )
Ok, modules loaded: Main.
>
> :t ServerConf
ServerConf :: String -> Int -> String -> ServerAuth -> ServerConf
>
> config1
ServerConf {serverHost = "0.0.0.0", serverPort = 8080, serverAssets = "/var/www", serverSecurity = AuthEmpty}
>
> config2
ServerConf {serverHost = "0.0.0.0", serverPort = 9090, serverAssets = "/home/arch/www", serverSecurity = AuthBasic "gh0st" "xmkg3p98sfawgav"}
>
> runServer config1
Running server at port 8080, listening host 0.0.0.0
Server Authentication = AuthEmpty
>
> runServer config2
Running server at port 9090, listening host 0.0.0.0
Server Authentication = AuthBasic "gh0st" "xmkg3p98sfawgav"
>
>
-- Read configuration file.
--
> readConfigFile "/tmp/server.conf"
Just (ServerConf {serverHost = "127.0.0.1", serverPort = 80, serverAssets = "/var/www", serverSecurity = AuthBasic "admin" "xjwxfm2pcfg56hga"})
>
> :t readConfigFile "/tmp/server.conf"
readConfigFile "/tmp/server.conf" :: IO (Maybe ServerConf)
>
> runServerConfigFile "/tmp/server.conf"
Running server at port 80, listening host 127.0.0.1
Server Authentication = AuthBasic "admin" "xjwxfm2pcfg56hga"
>
> runServerConfigFile "/etc/fstab"
Error: I can't read the server configuration file
>
Undefined can be used to define function signatures before they are implemented.
Example:
File: math1.hs - Before implement the functions.
add :: Int -> Int -> Int
add = undefined
sub :: Int -> Int -> Int
sub x y = x - y
product :: Num a => [a] -> a
product = undefined
Load in the Repl:
> :load /tmp/math1.hs
[1 of 1] Compiling Main ( /tmp/math1.hs, interpreted )
Ok, modules loaded: Main.
>
> :t add
add :: Int -> Int -> Int
>
> :t Main.add
Main.add :: Int -> Int -> Int
>
> :t product
<interactive>:1:1:
Ambiguous occurrence ‘product’
It could refer to either ‘Main.product’,
defined at /tmp/math1.hs:6:1
or ‘Prelude.product’,
imported from ‘Prelude’ at /tmp/math1.hs:1:1
(and originally defined in ‘Data.Foldable’)
>
> :t Main.product
Main.product :: Num a => [a] -> a
>
> Main.add 3 5
*** Exception: Prelude.undefined
>
> Main.product [1, 2, 3, 4, 5]
*** Exception: Prelude.undefined
>
File: math1.hs after implement the functions.
add :: Int -> Int -> Int
add = \x y -> x + y
sub :: Int -> Int -> Int
sub x y = x - y
product :: Num a => [a] -> a
product xs = foldr (*) 1 xs
Loading in the repl:
> :t add
add :: Int -> Int -> Int
>
> add 3 5
8
>
> Main.add 3 5
8
>
> :t product
<interactive>:1:1:
Ambiguous occurrence ‘product’
It could refer to either ‘Main.product’,
defined at /tmp/math2.hs:9:1
or ‘Prelude.product’,
imported from ‘Prelude’ at /tmp/math2.hs:1:1
(and originally defined in ‘Data.Foldable’)
>
>
> Main.product [1, 2, 3, 4, 5]
120
> :t Main.product
Main.product :: Num a => [a] -> a
>
The pattern match is useful to develop simple command line applications in a declarative and robust way.
File: calculator.hs
import System.Environment (getArgs)
import System.Exit (exitFailure, exitSuccess)
import Text.Read (readMaybe)
import Control.Monad
parseNum :: String -> Maybe Double
parseNum = readMaybe
parseNumList :: [String] -> Maybe [Double]
parseNumList xs = mapM readMaybe xs
{- | Perform operation on a single command line argument -}
doOperation :: Show b => (a -> b) -> (String -> Maybe a) -> String -> IO ()
doOperation fn fnParser arg =
case fnParser arg of
Just x -> do putStrLn $ "Result = " ++ show (fn x)
exitSuccess
Nothing -> do putStrLn $ "Error: Invalid input '" ++ arg ++ "'"
exitFailure
{- | Perform operation on multiple command line arguments -}
doListOperation :: Show b => ([a] -> b) -> ([String] -> Maybe [a]) -> [String] -> IO ()
doListOperation fn fnParser args =
case fnParser args of
Just xs -> do putStrLn $ "Result = " ++ show (fn xs)
exitSuccess
Nothing -> do putStrLn $ "Error: Invalid input '" ++ show args ++ "'"
exitFailure
showHelp = putStrLn $ unlines [
"Calculator App"
,"\nOptions\n"
," -help - Show help"
," -pi - Show pi number"
," -exp x - Compute exponential"
," -sin x - Compute sin"
," -echo file - Display file"
," -sum 10 20 30 - Compute sum"
," -prod 10 20 30 - Compute product"
]
echoFile file = do
content <- readFile file
putStrLn content
parseArgs :: [String] -> IO ()
parseArgs args =
case args of
[] -> showHelp >> exitSuccess
["-pi"] -> putStrLn (show 3.1415) >> exitSuccess
["-help"] -> showHelp >> exitSuccess
["-exp", x] -> doOperation exp parseNum x
["-sin", x] -> doOperation sin parseNum x
["-cos", x] -> doOperation cos parseNum x
["-echo", file] -> echoFile file
"-sum":args -> doListOperation sum parseNumList args
"-prod":args -> doListOperation product parseNumList args
_ -> do putStrLn "Error: Invalid command line switches."
exitFailure
{-
Or
main :: IO ()
main = do
args <- getArgs
parseArgs args
-}
main :: IO ()
main = getArgs >>= parseArgs
Command line tests:
Running in as script:
$ stack exec -- runhaskell /tmp/calculator.hs
Calculator App
Options
-help - Show help
-pi - Show pi number
-exp x - Compute exponential
-sin x - Compute sin
-echo file - Display file
-sum 10 20 30 - Compute sum
-prod 10 20 30 - Compute product
$ stack exec -- runhaskell /tmp/calculator.hs -help
Calculator App
Options
-help - Show help
-pi - Show pi number
-exp x - Compute exponential
-sin x - Compute sin
-echo file - Display file
-sum 10 20 30 - Compute sum
-prod 10 20 30 - Compute product
$ stack exec -- runhaskell /tmp/calculator.hs sada
Error: Invalid command line switches.
$ stack exec -- runhaskell /tmp/calculator.hs -exp 1.0
Result = 2.718281828459045
$ stack exec -- runhaskell /tmp/calculator.hs -exp 2.0
Result = 7.38905609893065
$ stack exec -- runhaskell /tmp/calculator.hs -sin 3.1415
Result = 9.265358966049026e-5
$ stack exec -- runhaskell /tmp/calculator.hs -cos x2323
Error: Invalid input 'x2323'
$ stack exec -- runhaskell /tmp/calculator.hs -cos
Error: Invalid command line switches.
$ stack exec -- runhaskell /tmp/calculator.hs -sum 1 2 3 4 5
Result = 15.0
$ stack exec -- runhaskell /tmp/calculator.hs -prod 1 2 3 4 5
Result = 120.0
$ stack exec -- runhaskell /tmp/calculator.hs -prod 1 2 3 sads 4 5
Error: Invalid input '["1","2","3","sads","4","5"]'
$ stack exec -- runhaskell /tmp/calculator.hs -echo /etc/issue
Manjaro Linux \r (\n) (\l)
Compiling:
$ stack exec -- ghc /tmp/calculator.hs -o /tmp/calculator.bin
[1 of 1] Compiling Main ( /tmp/calculator.hs, /tmp/calculator.o )
Linking /tmp/calculator.bin ...
$ file /tmp/calculator.bin
/tmp/calculator.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b3a0da3f4814ef85dd257b9e81651e2d234bb026, not stripped, with debug_info
$ /tmp/calculator.bin
Calculator App
Options
-help - Show help
-pi - Show pi number
-exp x - Compute exponential
-sin x - Compute sin
-echo file - Display file
-sum 10 20 30 - Compute sum
-prod 10 20 30 - Compute product
$ /tmp/calculator.bin -pi
3.1415
$ /tmp/calculator.bin -exp pi
Error: Invalid input 'pi'
$ /tmp/calculator.bin -exp 3.1415
Result = 23.138548663861286
$ /tmp/calculator.bin -sum 1 2 3 4 5 6 7
Result = 28.0
$ /tmp/calculator.bin -sum 1 nothing 3 4 5 6 asdas
Error: Invalid input '["1","nothing","3","4","5","6","asdas"]'
$ /tmp/calculator.bin -echo /etc/lsb-release
DISTRIB_ID=ManjaroLinux
DISTRIB_RELEASE=17.0
DISTRIB_CODENAME=Gellivara
DISTRIB_DESCRIPTION="Manjaro Linux"
Testing in REPL:
> :t parseArgs
parseArgs :: [String] -> IO ()
>
> parseArgs ["-help"]
Calculator App
Options
-help - Show help
-pi - Show pi number
-exp x - Compute exponential
-sin x - Compute sin
-echo file - Display file
-sum 10 20 30 - Compute sum
-prod 10 20 30 - Compute product
*** Exception: ExitSuccess
>
> parseArgs ["-help", "qasdas", "seds"]
Error: Invalid command line switches.
*** Exception: ExitFailure 1
>
> parseArgs ["-pi"]
3.1415
*** Exception: ExitSuccess
>
>
> parseArgs ["-exp", "1.23"]
Result = 3.4212295362896734
*** Exception: ExitSuccess
>
> parseArgs ["-sum", "1.23", "2.323", "4.23"]
Result = 7.783
*** Exception: ExitSuccess
>
> parseArgs ["-echo", "/etc/lsb-release"]
DISTRIB_ID=ManjaroLinux
DISTRIB_RELEASE=17.0
DISTRIB_CODENAME=Gellivara
DISTRIB_DESCRIPTION="Manjaro Linux"
>
The module Debug.Trace provides functions to allow debug computations.
- Module: Debug.Trace
Function | Signature | Description | |
---|---|---|---|
trace | :: | String -> a -> a | Print the first argument (string) and returns the second. |
traceShow | :: | Show a => a -> b -> b | Print the first value and returns the second. |
traceIO | :: | String -> IO () | Like trace but works in a IO Monad. |
Example:
import Debug.Trace
let x = 10 + 20 in trace ("the value of (10 + 20) is = " ++ show x) x
> the value of (10 + 20) is = 30
30
it
:{
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = trace ("n = " ++ show n) n * factorial (n -1)
:}
> factorial 5
n = 5
n = 4
n = 3
n = 2
n = 1
120
it :: Integer
>
> factorial 10
n = 10
n = 9
n = 8
n = 7
n = 6
n = 5
n = 4
n = 3
n = 2
n = 1
3628800
it :: Integer
>
:{
myfoldl :: Show acc => (acc -> x -> acc) -> acc -> [x] -> acc
myfoldl step acc [] = acc
myfoldl step acc (x:xs) = myfoldl step (step acc x) xs
:}
> myfoldl (\acc x -> 10 * acc +x) 0 [1, 2, 3, 4, 5]
12345
>
:{
myfoldl2 :: Show acc => (acc -> x -> acc) -> acc -> [x] -> acc
myfoldl2 step acc [] = acc
myfoldl2 step acc (x:xs) = let acc' = step acc x
in trace ("acc' = " ++ show acc') $ myfoldl2 step acc' xs
:}
> myfoldl2 (\acc x -> 10 * acc +x) 0 [1, 2, 3, 4, 5]
acc' = 1
acc' = 12
acc' = 123
acc' = 1234
acc' = 12345
12345
>
> let result = myfoldl2 (\acc x -> 10 * acc +x) 0 [1, 2, 3, 4, 5]
> result
acc' = 1
acc' = 12
acc' = 123
acc' = 1234
acc' = 12345
12345
> result
acc' = 1
acc' = 12
acc' = 123
acc' = 1234
acc' = 12345
12345
> result * 3
acc' = 1
acc' = 12
acc' = 123
acc' = 1234
acc' = 12345
37035
>
:{
traceLabel :: Show a => String -> a -> a
traceLabel label value = trace (label ++ show value) value
:}
:{
myfoldl3 :: Show acc => (acc -> x -> acc) -> acc -> [x] -> acc
myfoldl3 step acc [] = acc
myfoldl3 step acc (x:xs) = myfoldl3 step (traceLabel "acc' = " $ step acc x) xs
:}
> myfoldl3 (\acc x -> 10 * acc +x) 0 [1, 2, 3, 4, 5]
acc' = 1
acc' = 12
acc' = 123
acc' = 1234
acc' = 12345
12345
>
Example - GHCI without configuration file
$ stack ghci
Configuring GHCi with the following packages:
GHCi, version 8.0.1: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /tmp/ghci7713/ghci-script
Prelude>
Prelude> import Control.Monad
Prelude Control.Monad>
Prelude Control.Monad> import System.Directory as D
Prelude Control.Monad D>
Prelude Control.Monad D> D.getDirectoryContents "/etc/" >>= mapM_ putStrLn
systemd
motd
adobe
ld.so.cache
environment
libreoffice
rc_keymaps
sensors3.conf
gshadow
acpi
pkcs11
modules-load.d
cron.deny
shadow-
gufw
security
tmpfiles.d
... ...
Prelude Control.Monad D>
Prelude Control.Monad D> :{
Prelude Control.Monad D| getPassword :: String -> IO () -> IO () -> IO ()
Prelude Control.Monad D| getPassword password success error = do
Prelude Control.Monad D| passwd <- putStr "Enter the password: " >> getLine
Prelude Control.Monad D| if passwd == password
Prelude Control.Monad D| then success
Prelude Control.Monad D| else do error
Prelude Control.Monad D| getPassword password success error
Prelude Control.Monad D| :}
Prelude Control.Monad D>
Example - GHCI with configuration file
File: ~/.ghci
import Control.Concurrent (forkIO, threadDelay)
import System.Directory (getCurrentDirectory)
-- Set prompts
:set prompt "> "
:set prompt2 "- "
-- Print type after evaluation
:set +t
-- Start at tmp directory
:cd /tmp
-- Add language extensions
:set -XFlexibleInstances
putStrLn "Wellcome to Haskell"
GHCI Repl:
$ stack ghci
Configuring GHCi with the following packages:
GHCi, version 8.0.1: http://www.haskell.org/ghc/ :? for help
Wellcome to Haskell
it :: ()
Loaded GHCi configuration from /home/archbox/.ghci
Loaded GHCi configuration from /tmp/ghci8295/ghci-script
>
> import Control.Monad
> import System.Directory as D
>
> D.getDirectoryContents "/etc/" >>= mapM_ putStrLn
systemd
motd
adobe
ld.so.cache
environment
libreoffice
rc_keymaps
sensors3.conf
gshadow
... ... ...
-- Paste this function
{-
getPassword :: String -> IO () -> IO () -> IO ()
getPassword password success error = do
passwd <- putStr "Enter the password: " >> getLine
if passwd == password
then success
else do error
getPassword password success error
-}
> :{
- getPassword :: String -> IO () -> IO () -> IO ()
- getPassword password success error = do
- passwd <- putStr "Enter the password: " >> getLine
- if passwd == password
- then success
- else do error
- getPassword password success error
- :}
getPassword :: String -> IO () -> IO () -> IO ()
>
> let fn a b = 10 * a - b
fn :: Num a => a -> a -> a
> fn 10 20
80
it :: Num a => a
> it
80
it :: Num a => a
>
> :t forkIO
forkIO :: IO () -> IO GHC.Conc.Sync.ThreadId
> :t threadDelay
threadDelay :: Int -> IO ()
>
> forkIO (forever $ threadDelay 1000000 >> putStrLn "Repeat forever with 1 second delay")
ThreadId 82
it :: GHC.Conc.Sync.ThreadId
> Repeat forever with 1 second delay
Repeat forever with 1 second delay
Repeat forever with 1 second delay
Repeat forever with 1 second delay
Repeat forever with 1 second delay
Repeat forever with 1 second delay
Repeat forever with 1 second delay
Repeat forever with 1 second delay
.. ... ... ...
It is useful to extract the value wrapped by an IO action to small experiments and interactive commands. Note: It is only possible in the REPL.
Not possible because readFile returns an IO action.
> putStrLn (readFile "/etc/lsb-release")
<interactive>:5:11: error:
• Couldn't match type ‘IO String’ with ‘[Char]’
Expected type: String
Actual type: IO String
• In the first argument of ‘putStrLn’, namely
‘(readFile "/etc/lsb-release")’
In the expression: putStrLn (readFile "/etc/lsb-release")
In an equation for ‘it’:
it = putStrLn (readFile "/etc/lsb-release")
>
> :t readFile
readFile :: FilePath -> IO String
> let content = readFile "/etc/lsb-release"
> :t content
content :: IO String
>
> putStrLn content
<interactive>:9:10: error:
• Couldn't match type ‘IO String’ with ‘[Char]’
Expected type: String
Actual type: IO String
• In the first argument of ‘putStrLn’, namely ‘content’
In the expression: putStrLn content
In an equation for ‘it’: it = putStrLn content
>
> :t readFile
readFile :: FilePath -> IO String
Solutions:
Extract the IO action content. It is possible due to the Haskell REPL be inside an IO action.
> let contentIOaction = readFile "/etc/lsb-release"
> :t contentIOaction
contentIOaction :: IO String
>
> content <- contentIOaction
>
> :t content
content :: String
> putStrLn content
DISTRIB_ID=ManjaroLinux
DISTRIB_RELEASE=17.0.1
DISTRIB_CODENAME=Gellivara
DISTRIB_DESCRIPTION="Manjaro Linux"
> length content
110
>
> lines content
["DISTRIB_ID=ManjaroLinux","DISTRIB_RELEASE=17.0.1","DISTRIB_CODENAME=Gellivara","DISTRIB_DESCRIPTION=\"Manjaro Linux\""]
>
{- -- ======================== Or ======================= --}
> content <- readFile "/etc/lsb-release"
> :t content
content :: String
> putStrLn content
DISTRIB_ID=ManjaroLinux
DISTRIB_RELEASE=17.0.1
DISTRIB_CODENAME=Gellivara
DISTRIB_DESCRIPTION="Manjaro Linux"
> length content
110
>
Pass the IO action content using (>>=) bind or (=<<)
> readFile "/etc/lsb-release" >>= putStrLn
DISTRIB_ID=ManjaroLinux
DISTRIB_RELEASE=17.0.1
DISTRIB_CODENAME=Gellivara
DISTRIB_DESCRIPTION="Manjaro Linux"
> putStrLn =<< readFile "/etc/lsb-release"
DISTRIB_ID=ManjaroLinux
DISTRIB_RELEASE=17.0.1
DISTRIB_CODENAME=Gellivara
DISTRIB_DESCRIPTION="Manjaro Linux"
The Ghci directive :script is load interactive commands that could by typed in Ghci shell from a file.
File: src/script_ghci.hs
content <- readFile "/etc/lsb-release"
let nChars = length content
putStrLn $ "File content = \n" ++ content
putStrLn $ "File size (number of chars) = " ++ show nChars
putStrLn "--------------------------------"
:{
getPassword :: String -> IO () -> IO () -> IO ()
getPassword password success error = do
passwd <- putStr "Enter the password: " >> getLine
if passwd == password
then success
else do error
getPassword password success error
:}
:{
showSecreteFile :: String -> IO ()
showSecreteFile file = do
content <- readFile file
putStrLn "Secrete File Content"
putStrLn content
:}
:{
getPassword2 :: String -> Int -> IO ()
getPassword2 password maxTry = aux (maxTry - 1)
where
aux counter = do
passwd <- putStr "Enter the password: " >> getLine
case (counter, passwd) of
_ | passwd == password
-> showSecreteFile "/etc/shells"
_ | counter == 0
-> putStrLn $ "File destroyed after " ++ show maxTry ++ " attempts."
_
-> do putStrLn $ "Try again. You only have " ++ show counter ++ " attempts."
aux (counter - 1)
:}
let printLine = putStrLn "-----------------------------------------------"
printLine
putStrLn "Open secrete vault: "
getPassword "guessthepassword" (putStrLn "Vault opened. Ok.") (putStrLn "Error: Wrong password. Try Again")
printLine
putStrLn "Open secret file: (1)"
getPassword2 "xyzVjkmArchp972343asx" 3
printLine
putStrLn "Open secret file: (2)"
getPassword2 "xyzVjkmArchp972343asx" 3
Running:
archbox@ghostpc 18:19 ~/Documents/projects/fp-by-example.tutorial/haskell
$ stack ghci
Configuring GHCi with the following packages:
GHCi, version 8.0.1: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /home/archbox/.ghci
Loaded GHCi configuration from /tmp/ghci7633/ghci-script
>
>
> :script src/script_ghci.hs
File content =
DISTRIB_ID=ManjaroLinux
DISTRIB_RELEASE=17.0.1
DISTRIB_CODENAME=Gellivara
DISTRIB_DESCRIPTION="Manjaro Linux"
File size (number of chars) = 110
--------------------------------
-----------------------------------------------
Open secrete vault:
Enter the password: mxkasdasd
Error: Wrong password. Try Again
Enter the password: 32423asdas
Error: Wrong password. Try Again
Enter the password: guessht^?
Error: Wrong password. Try Again
Enter the password: guessthepassword
Vault opened. Ok.
-----------------------------------------------
Open secret file: (1)
Enter the password: 234asdqa
Try again. You only have 2 attempts.
Enter the password: i dont know
Try again. You only have 1 attempts.
Enter the password: try again
File destroyed after 3 attempts.
-----------------------------------------------
Open secret file: (2)
Enter the password: tell me it
Try again. You only have 2 attempts.
Enter the password: archxfmksjpfUif83432
Try again. You only have 1 attempts.
Enter the password: xyzVjkmArchp972343asx
Secrete File Content
#
# /etc/shells
#
/bin/sh
/bin/bash
# End of file
/bin/zsh
/usr/bin/zsh