Agents lang-haskell-library-dev
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.
install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/lang-haskell-library-dev" ~/.claude/skills/arustydev-agents-lang-haskell-library-dev && rm -rf "$T"
manifest:
content/skills/lang-haskell-library-dev/SKILL.mdsource content
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
- Foundational library patterns (API design, versioning, testing strategies)meta-library-dev
- Core Haskell language patterns and idiomslang-haskell-dev
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) | |
| New library (Stack) | |
| Build | / |
| Test | / |
| Documentation | / |
| REPL with library | / |
| Publish (dry run) | / |
| Upload to Hackage | |
| Check package | |
| Format code | or |
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 -
passes with no errorscabal check - 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
- Foundational library patternsmeta-library-dev
- Core Haskell fundamentalslang-haskell-dev- Haskell Package Versioning Policy
- Cabal User Guide
- Stack User Guide
- Hackage
- Haddock Documentation
- QuickCheck Manual