The Complete Magazine on Open Source

Access Redis and PostgreSQL Databases with Haskell

SHARE
/ 293 0
Postgre and redis Database visual
Named after logician Haskell Curry, Haskell is a standardised,general-purpose, purely functional programming language, with non-strict semantics and strong static typing. This tenth article on Haskell explores access to Redis and PostgreSQL databases using Haskell modules.

The Hackage website at https://hackage.haskell.org/packages/#cat:Database provides a vast number of database packages that you can use, a couple of which will be covered here.
You will need to install the cabal-install tool on Fedora, for example, using the following command:

$ sudo yum install cabal-install

Connecting to the Redis database
Let’s use the hedis package to connect to the Redis server. Install the Fedora dependency package alex, and the Redis server as shown below:

$ sudo yum install alex redis

You can then install the hedis package using the following commands:

$ cabal update
$ cabal install hedis

This installs the latest hedis version 0.6.5. You can now start the Redis server on Fedora using the service command:

$ sudo service redis start

You can then test connectivity to the Redis server using the redis-cli command by issuing the PING command as follows:

$ redis-cli

127.0.0.1:6379> PING
PONG

You can also test the same using the hedis package inside the GHCi interpreter as illustrated below:

$ ghci
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.

ghci> :m Database.Redis

ghci> conn <- connect defaultConnectInfo

Loading package array-0.4.0.1 ... linking ... done.
Loading package base-unicode-symbols-0.2.2.4 ... linking ... done.
Loading package deepseq-1.3.0.1 ... linking ... done.
Loading package old-locale-1.0.0.5 ... linking ... done.
Loading package time-1.4.0.1 ... linking ... done.
Loading package transformers-0.3.0.0 ... linking ... done.
Loading package bytestring-0.10.0.2 ... linking ... done.
Loading package text-0.11.3.1 ... linking ... done.
Loading package stm-2.4.2 ... linking ... done.
Loading package primitive-0.5.0.1 ... linking ... done.
Loading package vector-0.10.0.1 ... linking ... done.
Loading package hashable-1.1.2.5 ... linking ... done.
Loading package transformers-base-0.4.1 ... linking ... done.
Loading package monad-control-0.3.2.1 ... linking ... done.
Loading package containers-0.5.0.0 ... linking ... done.
Loading package attoparsec-0.10.4.0 ... linking ... done.
Loading package mtl-2.1.2 ... linking ... done.
Loading package BoundedChan-1.0.3.0 ... linking ... done.
Loading package bytestring-lexing-0.4.3.2 ... linking ... done.
Loading package unix-2.6.0.1 ... linking ... done.
Loading package network-2.6.0.2 ... linking ... done.
Loading package resource-pool-0.2.3.2 ... linking ... done.
Loading package hedis-0.6.5 ... linking ... done.

ghci> runRedis conn ping
Right Pong

I would recommend that you use defaultConnectInfo to connect to the database, and its type is ConnectInfo:

ghci> :t defaultConnectInfo
defaultConnectInfo :: ConnectInfo

The different options that can be used in defaultConnectInfo are as follows:

connectHost = “localhost”
connectPort = PortNumber 6379 -- Redis port
connectAuth = Nothing -- No authentication
connectDatabase = 0 -- SELECT database 0
connectMaxConnections = 10 -- Up to 10 connections
connectMaxIdleTime = 20 -- Keep connection open for 20 seconds

The types of conn, connect, runRedis and ping are given below:

ghci> :t conn
conn :: Connection

ghci> :t connect
connect :: ConnectInfo -> IO Connection

ghci> :t runRedis
runRedis :: Connection -> Redis a -> IO a

ghci> :t ping
ping :: RedisCtx m f => m (f Status)

If the Redis server was not started, and you tried to issue the ping command, the following exception would be automatically thrown by the package:

ghci> runRedis conn ping
*** Exception: connect: does not exist (No route to host)

You can automate the above code snippets into Haskell code with a main function, as demonstrated below:

{-# LANGUAGE OverloadedStrings #-}
import Database.Redis
main :: IO (Either Reply Status)
main = do
conn <- connect defaultConnectInfo
runRedis conn ping

The OverloadedStrings extension allows string literals to be polymorphic for the IsString class. You can compile and run the above code inside GHCi, as follows:

$ ghci ping.hs

GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main ( ping.hs, interpreted )
Ok, modules loaded: Main.

ghci> main
...
Right Pong

The echo Redis command is used to print a message that is passed as an argument to it. The equivalent hedis echo command expects the message to be of type ByteString. For example:

{-# LANGUAGE OverloadedStrings #-}
import Database.Redis
import qualified Data.ByteString as B

bytes :: B.ByteString
bytes = “Hello, World” :: B.ByteString

main :: IO (Either Reply B.ByteString)
main = do
conn <- connect defaultConnectInfo
runRedis conn $ echo bytes

Loading the above code in GHCi produces the following output:

ghci> main
Right “Hello, World”

The type signature of the echo function is as follows:

echo
:: RedisCtx m f =>
Data.ByteString.Internal.ByteString
-> m (f Data.ByteString.Internal.ByteString)

You can set a value to a key using the set function in hedis. An example is shown below:

{-# LANGUAGE OverloadedStrings #-}
import Database.Redis

main :: IO (Either Reply Status)
main = do
conn <- connect defaultConnectInfo
runRedis conn $ set “a” “apple”

Loading the above set.hs code in GHCi and testing the same produces the following output:

ghci> :l set.hs
[1 of 1] Compiling Main ( set.hs, interpreted )
Ok, modules loaded: Main.

ghci> main
Right Ok

The type signature of the set function is shown below:

ghci> :t set
set
:: RedisCtx m f =>
Data.ByteString.Internal.ByteString
-> Data.ByteString.Internal.ByteString -> m (f Status)

You can verify the value of the key ‘a’ from the redis-cli command, and it must return the value “apple”:

127.0.0.1:6379> get a
“apple”

You can also retrieve the value of a key using the get function. For example:

{-# LANGUAGE OverloadedStrings #-}
import Database.Redis
import Control.Monad.IO.Class

main :: IO ()
main = do
conn <- connect defaultConnectInfo
runRedis conn $ do
result <- get “a”
liftIO $ print result

Executing the above code in GHCi gives the expected result:

ghci> :l get.hs
[1 of 1] Compiling Main ( get.hs, interpreted )
Ok, modules loaded: Main.

ghci> main
Right (Just “apple”)

The liftIO function transforms an IO action into a Monad. Its type signature is shown below:

ghci> :t liftIO
liftIO :: MonadIO m => IO a -> m a
The type signature of the get function is as follows:
ghci> :t get
get
:: RedisCtx m f =>
Data.ByteString.Internal.ByteString
-> m (f (Maybe Data.ByteString.Internal.ByteString))

You are encouraged to read the Database.Redis documentation page that contains a comprehensive list of commands and their usage at https://hackage.haskell.org/package/hedis-0.6.5/docs/Database-Redis.html.

Accessing the PostgreSQL database
We shall now explore accessing a PostgreSQL database using the postgresql-simple (0.4.10.0) package. You will need to install and configure PostgreSQL for your GNU/Linux distribution. Please follow your distribution documentation to do so. On Fedora, for example, you can install the database server using the following command:

$ sudo yum install postgresql-server postgresql-contrib

You can then start the database server using the following service command:

$ sudo service postgresql start

You can now install the postgresql-simple package using the cabal command:

$ cabal install postgresql-simple

Let us first create a database and a schema using the Postgresql command-line utility psql:

$ psql -U postgres
Password for user postgres:
psql (9.3.5)
Type “help” for help.

postgres=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-------------+----------+-------------+-------------+--------
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
template0| postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
=c/ postgres + postgres=CTc/postgres
template1 | postgres | UTF8| en_US.UTF-8 | en_US.UTF-8 |
=c/postgres + postgres=CTc/postgres
(3 rows)

postgres=# CREATE DATABASE test;
CREATE DATABASE

postgres-# \c test

You are now connected to database ‘test’ as user ‘postgres’.

test=# create schema social;
CREATE SCHEMA

test=# \dn
public | postgres
social | postgres

We can then create a users’ table with an ID, first name and last name using the postgresql-simple package:

{-# LANGUAGE OverloadedStrings #-}

import Database.PostgreSQL.Simple

main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “postgres”
, connectPassword = “postgres123”
, connectDatabase = “test”
}

execute conn “create table social.users (id INT, fname VARCHAR(80), lname VARCHAR(80))” ()
close conn

Loading the above code in GHCi creates the table social.users as shown below:

$ ghci create.hs

GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main ( create.hs, interpreted )
Ok, modules loaded: Main.

ghci> main

Loading package array-0.4.0.1 ... linking ... done.
Loading package deepseq-1.3.0.1 ... linking ... done.
Loading package bytestring-0.10.0.2 ... linking ... done.
Loading package containers-0.5.0.0 ... linking ... done.
Loading package text-0.11.3.1 ... linking ... done.
Loading package attoparsec-0.10.4.0 ... linking ... done.
Loading package blaze-builder-0.3.1.1 ... linking ... done.
Loading package dlist-0.5 ... linking ... done.
Loading package hashable-1.1.2.5 ... linking ... done.
Loading package transformers-0.3.0.0 ... linking ... done.
Loading package mtl-2.1.2 ... linking ... done.
Loading package old-locale-1.0.0.5 ... linking ... done.
Loading package syb-0.4.0 ... linking ... done.
Loading package pretty-1.1.1.0 ... linking ... done.
Loading package template-haskell ... linking ... done.
Loading package time-1.4.0.1 ... linking ... done.
Loading package unordered-containers-0.2.3.0 ... linking ... done.
Loading package primitive-0.5.0.1 ... linking ... done.
Loading package vector-0.10.0.1 ... linking ... done.
Loading package aeson-0.6.2.1 ... linking ... done.
Loading package random-1.0.1.1 ... linking ... done.
Loading package scientific-0.2.0.2 ... linking ... done.
Loading package case-insensitive-1.0.0.1 ... linking ... done.
Loading package blaze-textual-0.2.0.8 ... linking ... done.
Loading package postgresql-libpq-0.9.0.2 ... linking ... done.
Loading package binary-0.7.4.0 ... linking ... done.
Loading package cereal-0.3.5.2 ... linking ... done.
Loading package entropy-0.2.2.1 ... linking ... done.
Loading package tagged-0.6 ... linking ... done.
Loading package crypto-api-0.11 ... linking ... done.
Loading package cryptohash-0.9.0 ... linking ... done.
Loading package network-info-0.2.0.5 ... linking ... done.
Loading package uuid-1.3.8 ... linking ... done.
Loading package postgresql-simple-0.4.10.0 ... linking ... done.

You can verify the created table from the psql prompt:

test=# \d social.users
id | integer |
fname | character varying(80) |
lname | character varying(80) |

You can also list the databases in the PostgreSQL server using the query_ function as illustrated below:

{-# LANGUAGE OverloadedStrings #-}
import Database.PostgreSQL.Simple
main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “postgres”
, connectPassword = “postgres123”
, connectDatabase = “test”
}
databases <- query_ conn “SELECT datname FROM pg_database”
print (databases :: [Only String])

close conn

Executing the above code in GHCi produces the following output:

$ ghci show.hs
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main ( show.hs, interpreted )
Ok, modules loaded: Main.

ghci> main
[Only {fromOnly = “template1”},Only {fromOnly = “template0”},Only {fromOnly = “postgres”},Only {fromOnly = “test”}]

You can now insert a record into the databases using the execute function:

execute conn “insert into social.users (id, fname, lname) values (?, ?, ?)” [“1” :: String, “Edwin” :: String, “Brady” :: String]

After executing the above code, you can verify the database entry from the psql prompt:

test=# select * from social.users;
id | fname | lname
----+-------+-------
1 | Edwin | Brady
(1 row)

You can also do batch inserts using the executeMany function. For example:

executeMany conn “insert into social.users (id, fname, lname) values (?, ?, ?)” [(“2” :: String, “Simon” :: String, “Marlow” :: String), (“3” :: String, “Ulf” :: String, “Norell” :: String)]

After running the above code, you can check the newly added rows in the database from the psql command-line tool:

test=# select * from social.users;
id | fname | lname
----+-------+--------
1 | Edwin | Brady
2 | Simon | Marlow
3 | Ulf | Norell
(3 rows)

You can also change a record entry using the UPDATE statement as shown below:

execute conn “update social.users SET lname = ‘Peyton Jones’ where fname = ‘Simon’” ()

The corresponding entry is updated as seen from the psql prompt:

test=# select * from social.users;
id | fname | lname
----+-------+--------------
1 | Edwin | Brady
3 | Ulf | Norell
2 | Simon | Peyton Jones
(3 rows)

It is recommended that you catch exceptions when running database commands. Consider the following example, where the number of arguments passed does not match with what is expected:

{-# LANGUAGE OverloadedStrings #-}

import Database.PostgreSQL.Simple
import Control.Exception
import GHC.Int

main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “postgres”
, connectPassword = “postgres123”
, connectDatabase = “test”
}
result <- try (execute conn “insert into social.users (id, fname, lname) values (?, ?, ?)” [“4” :: String, “Laurel” :: String]) :: IO (Either SomeException Int64)
case result of
Left ex -> putStrLn $ “Caught exception: “ ++ show ex
Right val -> putStrLn $ “The answer was: “ ++ show val
close conn

The error is observed when the main function is executed as shown below:

ghci> main
Caught exception: FormatError {fmtMessage = “3 ‘?’ characters, but 2 parameters”, fmtQuery = “insert into social.users (id, fname, lname) values (?, ?, ?)”, fmtParams = [“4”,”Laurel”]}

You can also retrieve multiple records from the database and use the results, with the help of a map function. An example is illustrated below:

{-# LANGUAGE OverloadedStrings #-}

import Database.PostgreSQL.Simple
import Control.Monad
import Data.Text as Text

main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “postgres”
, connectPassword = “postgres123”
, connectDatabase = “test”
}
users <- query_ conn “SELECT fname, lname FROM social.users”
forM_ users $ \(fname, lname) ->
putStrLn $ Text.unpack fname ++ “ “ ++ Text.unpack lname
close conn

The output after executing the above code in GHCi returns the actual data:

ghci> main
Edwin Brady
Ulf Norell
Simon Peyton Jones

Please refer to the Database.PostgreSQL.Simple documentation for more examples and usage at https://hackage.haskell.org/package/postgresql-simple-0.4.10.