Tips And Tricks
Table of Contents
1 Tips and Tricks
1.1 Overview
It is a collection of many tips and tricks for Haskell development.
1.2 Configuration files in Haskell Syntax
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 >
1.3 Using undefined to specify function signature before implementation
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 >
1.4 Using pattern matching for simple command line application
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" >
1.5 Debug Computations using trace
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 >
1.6 Haskell Interactive Shell - GHCI
1.6.1 GHCI configuration file
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 .. ... ... ...
1.6.2 Extract contents of IO action
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"
1.6.3 Load interactive commands from file
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