WalzoneInterview Prep
📞 Interviewing soon? Practice with a realistic AI mock phone interview — it calls you, then scores you. First 15 min FREE →

Haskell · Intermediate · question 34 of 100

How do you work with databases in Haskell? Can you mention a few libraries?

📕 Buy this interview preparation book: 100 Haskell questions & answers — PDF + EPUB for $5

Working with databases in Haskell usually involves using libraries to interface with the database system and to provide a functional and type-safe way of querying and manipulating data. There are several popular libraries for working with databases in Haskell. I’ll discuss a few notable ones and provide some examples.

1. **persistent**

‘persistent‘ is a popular library for working with databases in Haskell. It provides a DSL for defining your data model and can work with different database backends like PostgreSQL, SQLite, and MySQL.

To use ‘persistent‘, first install it and the corresponding backend package, e.g., ‘persistent-postgresql‘. Create a file named ‘Model.hs‘ with a data model description. For example, a data model with a "Person" table might look like this:

{-# LANGUAGE EmptyDataDecls       #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE GADTs                #-}
{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE QuasiQuotes          #-}
{-# LANGUAGE TemplateHaskell      #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import Database.Persist.TH
import Data.Text (Text)

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person json
    name Text
    age Int
    deriving Show
|]

Note the ‘QuasiQuotes‘ and ‘TemplateHaskell‘ language extensions that enable the domain-specific language (DSL) for describing the schema.

Next, connect to the database and run queries:

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.IO.Class  (liftIO)
import Control.Monad.Logger (runStdoutLoggingT)
import Control.Monad.Trans.Resource (runResourceT)

import Database.Persist
import Database.Persist.Postgresql

import Model

connStr = "host=localhost dbname=test_db user=test_user password=test_password port=5432"

main :: IO ()
main = runStdoutLoggingT $ withPostgresqlConn connStr $ backend ->
  runResourceT $ runNoLoggingT $ runConn backend

runConn :: SqlBackend -> IO ()
runConn conn = do
    runSqlConn (runMigration migrateAll) conn
    johnId <- insert' conn $ Person "John Doe" 30
    janeId <- insert' conn $ Person "Jane Doe" 28
    people <- selectList' conn []
    liftIO $ print (people :: [Entity Person])

insert' :: SqlBackend -> Person -> IO (Key Person)
insert' = flip runSqlConn . insert

selectList' :: SqlBackend -> [Filter Person] -> IO [Entity Person]
selectList' = flip runSqlConn . (f -> selectList f [])

This code creates a connection to a PostgreSQL database, applies the migration to create the "Person" table if necessary, and then inserts two example records, and queries them back.

2. **esqueleto**

‘esqueleto‘ is a library built on top of ‘persistent‘ that provides a richer and more flexible DSL for constructing SQL queries. It adds support for joins, group by, aggregates, and more complex SQL constructs that aren’t supported natively by ‘persistent‘.

To try ‘esqueleto‘, install it and import the ‘Database.Esqueleto‘ module. Use the query DSL to write more advanced queries. For example, to retrieve all "Person" records with age above 29:

{-# LANGUAGE OverloadedStrings #-}
import Database.Esqueleto
import Model

-- ...

runConn :: SqlBackend -> IO ()
runConn conn = do
    runSqlConn (runMigration migrateAll) conn
    johnId <- insert' conn $ Person "John Doe" 30
    janeId <- insert' conn $ Person "Jane Doe" 28
    people <- selectOlderPeople conn 29
    liftIO $ print (people :: [Entity Person])

selectOlderPeople :: SqlBackend -> Int -> IO [Entity Person]
selectOlderPeople conn age = flip runSqlConn conn $ do
  select $
    from $ person -> do
    where_ (person ^. PersonAge >. val age)
    return person

3. **opaleye**

‘opaleye‘ is another library for interfacing with PostgreSQL databases specifically. It encourages a more type-safe approach by providing a type-level DSL for constructing queries and mapping them to Haskell types.

Here’s an example of using ‘opaleye‘ to declare a "Person" table, insert a record, and query the table:

{-# LANGUAGE Arrows #-}

import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import Data.Text (Text)
import Opaleye

data Person' a b = Person
  { personName :: a,
    personAge :: b
  }

type Person = Person' Text Int
type PersonColumn = Person' (Column PGText) (Column PGInt4)
type PersonTable = Table PersonColumn PersonColumn

personTable :: PersonTable
personTable =
  Table
    "person"
    (p2 (Person {personName = required "name", personAge = required "age"}))

$(makeAdaptorAndInstance "pPerson" ''Person')

personToInsert :: Person -> PersonColumn
personToInsert = pPerson Person

main :: IO ()
main = do
  con <- connect defaultConnectInfo {connectDatabase = "test_db"}
  runInsert con personTable (personToInsert (Person "John Doe" 30))
  people <- runQuery con (queryTable personTable)
  mapM_ print people

Choose a library based on the specific requirements of your application, such as the need for complex SQL queries or support for specific database systems. For starters, ‘persistent‘ with ‘esqueleto‘ is a good choice because of its flexibility, its intuitive DSL, and support for multiple database systems.

Remember that examples provided for each library will require specific packages to be added to the ‘.cabal‘ or ‘package.yaml‘ file dependencies.

Reading is step one. Saying it out loud is the interview. Our AI interviewer calls your phone and runs a realistic Haskell interview — then scores it.
📞 Practice Haskell — free 15 min
📕 Buy this interview preparation book: 100 Haskell questions & answers — PDF + EPUB for $5

All 100 Haskell questions · All topics