Agent Skills: Haskell Library Development

Haskell-specific library development patterns. Use when creating Haskell libraries, designing type-safe public APIs, organizing Cabal/Stack projects, writing property-based tests with QuickCheck, generating Haddock documentation, or publishing to Hackage. Extends meta-library-dev with Haskell tooling and idioms.

UncategorizedID: arustydev/ai/lang-haskell-library-dev

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/lang-haskell-library-dev

Skill Files

Browse the full folder contents for lang-haskell-library-dev.

Download Skill

Loading file tree…

content/skills/lang-haskell-library-dev/SKILL.md

Skill Metadata

Name
lang-haskell-library-dev
Description
Haskell-specific library development patterns. Use when creating Haskell libraries, designing type-safe public APIs, organizing Cabal/Stack projects, writing property-based tests with QuickCheck, generating Haddock documentation, or publishing to Hackage. Extends meta-library-dev with Haskell tooling and idioms.

Haskell Library Development

Haskell-specific patterns for library development. This skill extends meta-library-dev with Haskell tooling, type-driven design principles, and ecosystem practices.

This Skill Extends

  • meta-library-dev - Foundational library patterns (API design, versioning, testing strategies)
  • lang-haskell-dev - Core Haskell language patterns and idioms

For general library concepts like semantic versioning and testing pyramids, see meta-library-dev. For Haskell fundamentals like type classes and monads, see lang-haskell-dev.

This Skill Adds

  • Build tooling: Cabal, Stack, package.yaml configuration
  • Type-driven API design: Leveraging Haskell's type system for library interfaces
  • Module organization: Best practices for exposing clean public APIs
  • Property-based testing: QuickCheck patterns for library validation
  • Haddock documentation: API documentation standards
  • Hackage publishing: Package distribution and maintenance

This Skill Does NOT Cover

  • General library patterns - see meta-library-dev
  • Core Haskell language features - see lang-haskell-dev
  • Advanced type system features (GADTs, Type Families) - see lang-haskell-advanced-dev
  • Web frameworks (Servant, Yesod) - see framework-specific skills
  • Application development - see lang-haskell-app-dev

Quick Reference

| Task | Command/Pattern | |------|-----------------| | New library (Cabal) | cabal init --lib <name> | | New library (Stack) | stack new <name> simple-library | | Build | cabal build / stack build | | Test | cabal test / stack test | | Documentation | cabal haddock / stack haddock | | REPL with library | cabal repl / stack ghci | | Publish (dry run) | cabal sdist / stack sdist | | Upload to Hackage | cabal upload <tarball> | | Check package | cabal check | | Format code | fourmolu or ormolu |


Project Structure

Standard Library Layout

my-library/
├── my-library.cabal          # Cabal package description
├── stack.yaml                # Stack configuration (optional)
├── package.yaml              # hpack configuration (Stack alternative)
├── LICENSE
├── README.md
├── CHANGELOG.md
├── src/
│   ├── MyLibrary.hs          # Main module (re-exports public API)
│   ├── MyLibrary/
│   │   ├── Types.hs          # Public types
│   │   ├── Core.hs           # Core functionality
│   │   └── Internal.hs       # Internal implementation
│   └── MyLibrary/Internal/   # Private modules
│       └── Helpers.hs
├── test/
│   ├── Spec.hs               # Test entry point
│   ├── MyLibrary/
│   │   ├── TypesSpec.hs      # Unit tests
│   │   └── CoreSpec.hs
│   └── Properties.hs         # QuickCheck properties
├── benchmark/
│   └── Main.hs               # Criterion benchmarks
└── examples/
    └── Basic.hs              # Usage examples

Module Visibility

-- src/MyLibrary.hs (main entry point)
module MyLibrary
  ( -- * Core Types
    Config(..)
  , Document
  , ParseResult

    -- * Construction
  , defaultConfig
  , mkDocument

    -- * Operations
  , parse
  , render
  , validate

    -- * Error Types
  , ParseError(..)
  , ValidationError(..)

    -- * Re-export specific modules for advanced use
  , module MyLibrary.Types
  ) where

import MyLibrary.Types
import MyLibrary.Core
import qualified MyLibrary.Internal as Internal

-- Re-export selected functions
parse = Internal.parseImpl
render = Internal.renderImpl

Cabal Configuration

Required Fields (.cabal file)

cabal-version:      3.0
name:               my-library
version:            0.1.0.0
synopsis:           A brief one-line description
description:
    A longer description of what this library does.
    .
    Multiple paragraphs are separated by a single dot line.

homepage:           https://github.com/username/my-library
bug-reports:        https://github.com/username/my-library/issues
license:            BSD-3-Clause
license-file:       LICENSE
author:             Your Name
maintainer:         you@example.com
copyright:          2025 Your Name
category:           Data
build-type:         Simple
tested-with:        GHC == 9.4.8
                  , GHC == 9.6.3
                  , GHC == 9.8.1

extra-doc-files:
    CHANGELOG.md
    README.md

extra-source-files:
    examples/*.hs

Library Stanza

library
  exposed-modules:
      MyLibrary
      MyLibrary.Types
      MyLibrary.Core

  other-modules:
      MyLibrary.Internal
      MyLibrary.Internal.Helpers

  -- Modules included but not exposed
  reexported-modules:
      Data.Text as MyLibrary.Text

  hs-source-dirs:      src
  default-language:    Haskell2010
  default-extensions:
      OverloadedStrings
      DeriveGeneric
      LambdaCase

  ghc-options:
      -Wall
      -Wcompat
      -Widentities
      -Wincomplete-record-updates
      -Wincomplete-uni-patterns
      -Wmissing-home-modules
      -Wpartial-fields
      -Wredundant-constraints

  build-depends:
      base              >= 4.14 && < 5
    , text              >= 1.2  && < 2.2
    , containers        >= 0.6  && < 0.8
    , bytestring        >= 0.10 && < 0.13

Test Suite Stanza

test-suite my-library-test
  type:                exitcode-stdio-1.0
  hs-source-dirs:      test
  main-is:             Spec.hs
  other-modules:
      MyLibrary.TypesSpec
      MyLibrary.CoreSpec
      Properties

  default-language:    Haskell2010
  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N

  build-depends:
      base
    , my-library
    , hspec             >= 2.7  && < 3
    , QuickCheck        >= 2.14 && < 3
    , hspec-discover    >= 2.7  && < 3

  build-tool-depends:
      hspec-discover:hspec-discover

Benchmark Stanza

benchmark my-library-bench
  type:                exitcode-stdio-1.0
  hs-source-dirs:      benchmark
  main-is:             Main.hs
  default-language:    Haskell2010
  ghc-options:         -Wall -O2 -threaded -rtsopts -with-rtsopts=-N

  build-depends:
      base
    , my-library
    , criterion         >= 1.5 && < 2

Stack Configuration

stack.yaml

resolver: lts-22.7  # GHC 9.6.3

packages:
  - .

extra-deps: []

# Recommended flags
flags: {}

# Build options
ghc-options:
  "$locals": -Wall -Werror=incomplete-patterns

# Testing options
test:
  arguments:
    additional-args:
      - --color
      - --format=progress

package.yaml (hpack alternative to .cabal)

name: my-library
version: 0.1.0.0
synopsis: A brief one-line description
description: |
  A longer description of what this library does.

  Multiple paragraphs are supported.

github: username/my-library
license: BSD-3-Clause
author: Your Name
maintainer: you@example.com
copyright: 2025 Your Name
category: Data

extra-source-files:
  - README.md
  - CHANGELOG.md

dependencies:
  - base >= 4.14 && < 5
  - text >= 1.2 && < 2.2
  - containers >= 0.6 && < 0.8

ghc-options:
  - -Wall
  - -Wcompat
  - -Widentities
  - -Wincomplete-record-updates

default-extensions:
  - OverloadedStrings
  - DeriveGeneric
  - LambdaCase

library:
  source-dirs: src
  exposed-modules:
    - MyLibrary
    - MyLibrary.Types
    - MyLibrary.Core

tests:
  my-library-test:
    main: Spec.hs
    source-dirs: test
    ghc-options:
      - -threaded
      - -rtsopts
      - -with-rtsopts=-N
    dependencies:
      - my-library
      - hspec
      - QuickCheck
    build-tools:
      - hspec-discover

benchmarks:
  my-library-bench:
    main: Main.hs
    source-dirs: benchmark
    ghc-options:
      - -O2
      - -threaded
      - -rtsopts
    dependencies:
      - my-library
      - criterion

Type-Driven API Design

Smart Constructors

-- Hide constructor, export smart constructor
module MyLibrary.Types
  ( Email       -- Type exported without constructor
  , mkEmail     -- Smart constructor
  , emailText   -- Accessor
  ) where

import Data.Text (Text)
import qualified Data.Text as T

-- Opaque type
newtype Email = Email Text
  deriving (Show, Eq)

-- Smart constructor with validation
mkEmail :: Text -> Either String Email
mkEmail input
  | T.null input = Left "Email cannot be empty"
  | '@' `T.elem` input = Right (Email input)
  | otherwise = Left "Invalid email format"

-- Safe accessor
emailText :: Email -> Text
emailText (Email txt) = txt

Phantom Types for Type Safety

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}

-- State machine encoded in types
data State = Draft | Published

newtype Document (s :: State) = Document Text

-- Only drafts can be edited
editDocument :: Text -> Document 'Draft -> Document 'Draft
editDocument newText _ = Document newText

-- Only drafts can be published
publishDocument :: Document 'Draft -> Document 'Published
publishDocument (Document txt) = Document txt

-- Published documents can be rendered
renderDocument :: Document 'Published -> Html
renderDocument (Document txt) = toHtml txt

-- Type-safe workflow
workflow :: Html
workflow =
  renderDocument $
    publishDocument $
      editDocument "Updated content" initialDraft

Builder Pattern with Phantom Types

data Incomplete
data Complete

data ConfigBuilder (s :: Type) = ConfigBuilder
  { _timeout :: Maybe Int
  , _retries :: Maybe Int
  , _endpoint :: Maybe String
  }

-- Start with incomplete config
emptyConfig :: ConfigBuilder Incomplete
emptyConfig = ConfigBuilder Nothing Nothing Nothing

-- Builder functions return incomplete
setTimeout :: Int -> ConfigBuilder s -> ConfigBuilder Incomplete
setTimeout t cfg = cfg { _timeout = Just t }

setRetries :: Int -> ConfigBuilder s -> ConfigBuilder Incomplete
setRetries r cfg = cfg { _retries = Just r }

-- Only setEndpoint returns Complete
setEndpoint :: String -> ConfigBuilder s -> ConfigBuilder Complete
setEndpoint e cfg = cfg { _endpoint = Just e }

-- Can only build from Complete
build :: ConfigBuilder Complete -> Config
build (ConfigBuilder (Just t) (Just r) (Just e)) =
  Config t r e
build _ = error "Impossible: Complete builder guarantees all fields set"

-- Usage: Type system enforces all fields are set
config = build $ setEndpoint "https://api.example.com"
              $ setTimeout 30
              $ setRetries 3 emptyConfig

Leveraging Type Classes for Polymorphism

-- Generic serialization interface
class Serialize a where
  serialize :: a -> ByteString
  deserialize :: ByteString -> Either String a

-- Provide instances for your types
instance Serialize Document where
  serialize = encodeDocument
  deserialize = decodeDocument

-- Functions polymorphic over Serialize
saveToFile :: Serialize a => FilePath -> a -> IO ()
saveToFile path value = BS.writeFile path (serialize value)

loadFromFile :: Serialize a => FilePath -> IO (Either String a)
loadFromFile path = deserialize <$> BS.readFile path

Module Organization Patterns

Internal Modules

-- MyLibrary/Internal.hs
-- This module is in other-modules, not exposed-modules
module MyLibrary.Internal where

-- Internal functions used across the library
-- but not part of public API
internalHelper :: Text -> Result
internalHelper = ...

Hierarchical Re-exports

-- MyLibrary.hs (main entry point)
module MyLibrary
  ( -- * Re-export everything from Types
    module MyLibrary.Types
    -- * Re-export selected functions from Core
  , parse
  , validate
  , render
  ) where

import MyLibrary.Types
import MyLibrary.Core (parse, validate, render)

Qualified Re-exports

-- For users who want namespaced access
import qualified MyLibrary as ML
import qualified MyLibrary.Advanced as ML.Advanced

document = ML.parse input
result = ML.Advanced.complexOperation document

Property-Based Testing with QuickCheck

Arbitrary Instances

{-# LANGUAGE DeriveGeneric #-}

import Test.QuickCheck
import Test.QuickCheck.Arbitrary.Generic

data Document = Document
  { docTitle :: Text
  , docContent :: Text
  , docTags :: [Text]
  } deriving (Show, Eq, Generic)

-- Automatic Arbitrary instance
instance Arbitrary Document where
  arbitrary = genericArbitrary

-- Or custom generators
instance Arbitrary Document where
  arbitrary = Document
    <$> genTitle
    <*> genContent
    <*> listOf genTag
    where
      genTitle = T.pack <$> listOf1 (choose ('a', 'z'))
      genContent = T.pack <$> listOf (choose ('a', 'z'))
      genTag = elements ["haskell", "library", "testing"]

-- Constrained generators
genValidEmail :: Gen Email
genValidEmail = do
  user <- listOf1 (choose ('a', 'z'))
  domain <- elements ["example.com", "test.org"]
  return $ Email (T.pack $ user ++ "@" ++ domain)

Properties

-- test/Properties.hs
import Test.Hspec
import Test.QuickCheck
import MyLibrary

spec :: Spec
spec = describe "Document properties" $ do

  describe "parse . render = id" $
    it "roundtrips successfully" $
      property $ \doc ->
        parse (render doc) === Right doc

  describe "parse validates input" $
    it "rejects empty titles" $
      property $ \content tags ->
        let doc = Document "" content tags
        in parse (render doc) `shouldSatisfy` isLeft

  describe "tag operations are idempotent" $
    it "adding same tag twice = adding once" $
      property $ \doc tag ->
        addTag tag (addTag tag doc) === addTag tag doc

  describe "parsing is total (never throws)" $
    it "handles arbitrary input" $
      property $ \(input :: Text) ->
        case parse input of
          Left _ -> True
          Right _ -> True

Conditional Properties

prop_sortedListHead :: [Int] -> Property
prop_sortedListHead xs =
  not (null xs) ==>  -- Precondition
  head (sort xs) === minimum xs

-- Better: Use forAll with constrained generator
prop_sortedListHead' :: Property
prop_sortedListHead' =
  forAll (listOf1 arbitrary) $ \xs ->
    head (sort xs) === minimum xs

Invariants

-- Check invariants hold after operations
prop_balancedAfterInsert :: Key -> Value -> Tree -> Bool
prop_balancedAfterInsert k v tree =
  isBalanced (insert k v tree)

prop_sizeAfterInsert :: Key -> Value -> Tree -> Property
prop_sizeAfterInsert k v tree =
  k `notMember` tree ==>
  size (insert k v tree) === size tree + 1

Haddock Documentation

Module Documentation

{-|
Module      : MyLibrary
Description : Brief description of module purpose
Copyright   : (c) Your Name, 2025
License     : BSD-3-Clause
Maintainer  : you@example.com
Stability   : experimental
Portability : POSIX

Longer description of what this module provides.

= Usage

Basic usage example:

>>> import MyLibrary
>>> let doc = mkDocument "Hello"
>>> render doc
"<document>Hello</document>"

= Advanced Usage

More complex patterns and use cases.
-}
module MyLibrary where

Function Documentation

-- | Parse a document from text.
--
-- This function validates the input and returns either
-- a parse error or a valid document.
--
-- ==== Examples
--
-- Basic usage:
--
-- >>> parse "title: Hello\ncontent: World"
-- Right (Document {docTitle = "Hello", docContent = "World"})
--
-- Invalid input:
--
-- >>> parse ""
-- Left "Empty input"
--
-- ==== Notes
--
-- * Input must be UTF-8 encoded
-- * Title is required
-- * Content may be empty
--
parse :: Text -> Either ParseError Document
parse = ...

-- | Render a document to text.
--
-- The output format is compatible with 'parse':
--
-- prop> parse (render doc) == Right doc
--
render :: Document -> Text
render = ...

Documenting Types

-- | Configuration for the parser.
--
-- Use 'defaultConfig' or the builder pattern to construct.
data Config = Config
  { configTimeout :: Int
    -- ^ Timeout in seconds (must be positive)
  , configStrict :: Bool
    -- ^ Enable strict parsing mode
  , configEncoding :: Encoding
    -- ^ Character encoding to use
  } deriving (Show, Eq)

-- | Default configuration.
--
-- Equivalent to:
--
-- @
-- Config
--   { configTimeout = 30
--   , configStrict = False
--   , configEncoding = UTF8
--   }
-- @
defaultConfig :: Config
defaultConfig = Config 30 False UTF8

Sections and Organization

module MyLibrary
  ( -- * Types
    Document(..)
  , ParseError(..)

    -- * Construction
  , mkDocument
  , defaultDocument

    -- * Operations
    -- ** Parsing
  , parse
  , parseStrict
    -- ** Rendering
  , render
  , renderPretty

    -- * Utilities
  , validate
  , normalize
  ) where

Code Examples in Documentation

-- | Batch process multiple documents.
--
-- >>> let docs = [mkDocument "A", mkDocument "B"]
-- >>> mapM_ (print . render) docs
-- "<document>A</document>"
-- "<document>B</document>"
--
-- __Warning:__ This loads all documents into memory.
-- For large batches, use 'streamProcess' instead.
processBatch :: [Document] -> IO ()
processBatch = ...

Publishing to Hackage

Pre-publish Checklist

  • [ ] Version bumped in .cabal (follow PVP)
  • [ ] CHANGELOG.md updated
  • [ ] All tests pass: cabal test --test-show-details=direct
  • [ ] Documentation builds: cabal haddock
  • [ ] Package builds: cabal build all
  • [ ] No warnings: cabal build -Wall -Werror
  • [ ] cabal check passes with no errors
  • [ ] README.md is current and accurate
  • [ ] LICENSE file included
  • [ ] Tested with multiple GHC versions
  • [ ] Hackage account credentials configured

Package Versioning Policy (PVP)

Haskell uses PVP (A.B.C.D):

| Change Type | Version Update | Example | |-------------|----------------|---------| | Breaking API change | A.B → (A+1).0 | 1.2.0.0 → 2.0.0.0 | | New functionality (compatible) | A.B.C → A.(B+1).0 | 1.2.3.0 → 1.3.0.0 | | Bug fix (no API change) | A.B.C.D → A.B.C.(D+1) | 1.2.3.0 → 1.2.3.1 |

-- Before: version 0.1.0.0
-- After adding compatible function: version 0.2.0.0
-- After breaking change: version 1.0.0.0

Version Bounds

build-depends:
    base              >= 4.14 && < 5
    -- Allow major version 4.14 through 4.x, but not 5.x

  , text              >= 1.2  && < 1.3
    -- Allow 1.2.x versions only

  , containers        >= 0.6  && < 0.8
    -- Allow 0.6.x and 0.7.x

Building Distribution Tarball

# Check package is ready
cabal check

# Build source distribution
cabal sdist

# Check the tarball builds
cabal upload --publish <dist-newstyle/sdist/my-library-0.1.0.0.tar.gz> --dry-run

# Extract and test the tarball
cd /tmp
tar xzf my-library-0.1.0.0.tar.gz
cd my-library-0.1.0.0
cabal build
cabal test

Uploading to Hackage

# First-time setup: Create ~/.cabal/config with Hackage credentials
# Or use environment variable
export HACKAGE_USERNAME=yourusername
export HACKAGE_PASSWORD=yourpassword

# Upload as candidate (test on Hackage but not published)
cabal upload path/to/my-library-0.1.0.0.tar.gz

# Publish from candidate
cabal upload --publish path/to/my-library-0.1.0.0.tar.gz

# Check package page
open https://hackage.haskell.org/package/my-library

Documentation Upload

# Build documentation
cabal haddock --haddock-for-hackage

# Upload to Hackage
cabal upload -d <dist-newstyle/.../my-library-0.1.0.0-docs.tar.gz>

Deprecating Packages

-- In .cabal file
x-deprecated: Please use better-library instead

Testing Patterns

Unit Tests with HSpec

-- test/MyLibrary/CoreSpec.hs
module MyLibrary.CoreSpec (spec) where

import Test.Hspec
import MyLibrary

spec :: Spec
spec = describe "Core functionality" $ do

  describe "parse" $ do
    it "parses valid input" $ do
      parse "title: Hello" `shouldBe` Right expectedDoc

    it "rejects empty input" $ do
      parse "" `shouldSatisfy` isLeft

    it "handles unicode" $ do
      parse "title: こんにちは" `shouldSatisfy` isRight

  describe "render" $ do
    it "produces valid output" $ do
      let doc = mkDocument "Test"
      render doc `shouldContain` "Test"

    context "with empty document" $ do
      it "returns empty string" $ do
        render emptyDocument `shouldBe` ""

Test Organization

-- test/Spec.hs
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

-- hspec-discover automatically finds all *Spec.hs files

Testing with Temporary Files

import System.IO.Temp (withSystemTempFile)

spec = describe "File operations" $
  it "saves and loads documents" $
    withSystemTempFile "test.doc" $ \path handle -> do
      hClose handle
      saveDocument path doc
      loaded <- loadDocument path
      loaded `shouldBe` Right doc

Common Dependencies

Core Libraries

build-depends:
    base              >= 4.14 && < 5
  , text              >= 1.2  && < 2.2
  , bytestring        >= 0.10 && < 0.13
  , containers        >= 0.6  && < 0.8

Parsing

  , attoparsec        >= 0.13 && < 0.15
  , megaparsec        >= 9.0  && < 10
  , parser-combinators >= 1.2 && < 2

Error Handling

  , exceptions        >= 0.10 && < 0.11
  , transformers      >= 0.5  && < 0.7

Testing

  , hspec             >= 2.7  && < 3
  , QuickCheck        >= 2.14 && < 3
  , hspec-discover    >= 2.7  && < 3

JSON

  , aeson             >= 2.0  && < 2.3

Anti-Patterns

1. Exposing Too Much

-- Bad: Exposes internal implementation
module MyLibrary (module X) where
import MyLibrary.Internal as X

-- Good: Selective exports
module MyLibrary
  ( Document
  , parse
  , render
  ) where

2. Partial Functions in Public API

-- Bad: Can throw exception
head' :: [a] -> a
head' (x:_) = x

-- Good: Total function
headMaybe :: [a] -> Maybe a
headMaybe (x:_) = Just x
headMaybe [] = Nothing

3. Over-Constrained Version Bounds

-- Bad: Too restrictive
build-depends: text == 1.2.4.0

-- Good: PVP-compatible range
build-depends: text >= 1.2 && < 1.3

4. Missing Upper Bounds

-- Bad: No upper bound (will break on major updates)
build-depends: aeson >= 2.0

-- Good: PVP upper bound
build-depends: aeson >= 2.0 && < 2.3

Troubleshooting

Cabal Hell (Dependency Conflicts)

Problem: Cannot satisfy dependency requirements

Resolving dependencies...
Error: Could not resolve dependencies

Fix: Use cabal's new-style builds (default in modern Cabal):

# Clear cache and rebuild
cabal clean
rm -rf dist-newstyle
cabal build

# Or use Stack which uses curated snapshots
stack build

Haddock Fails to Build

Problem: Haddock parse errors

Fix: Check for:

  • Unbalanced delimiters in documentation comments
  • Invalid Haddock syntax (>>> for examples, @ for code)
  • Missing closing brackets in sections
# Build with verbose output
cabal haddock --haddock-options="--verbose"

Package Check Errors

Problem: cabal check reports warnings/errors

Fix: Address each error:

  • Add missing fields (synopsis, description, license)
  • Fix version bounds
  • Include all necessary files in extra-source-files

Tests Pass Locally but Fail in CI

Problem: Different GHC versions or dependencies

Fix:

  • Test locally with multiple GHC versions using Stack
  • Use matrix builds in CI
  • Pin resolver versions in stack.yaml
# .github/workflows/ci.yml
strategy:
  matrix:
    ghc: ['9.2.8', '9.4.8', '9.6.3']

References