Practical Haskell skill for Codex

Haskell that builds clean and has no partial functions. Codex orients in a Haskell project by reading the cabal or Stack files and GHC version, then keeps logic pure and IO at the edges, replaces partial functions with total ones, and uses Text and strict containers to avoid space leaks. GHC type errors are decoded with a lookup table and typed holes rather than guessed annotations, and changes are checked with -Wall, HLint, hspec and QuickCheck.

  • Languages
  • $haskell-basics
  • 181 lines
  • 2 files
  • ~1,800 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- haskell-basics

Installs to ~/.agents/skills/haskell-basics/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $haskell-basics, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Reads, writes and fixes practical Haskell with GHC, cabal or Stack - project layout, Text over String, Maybe and Either error handling, records and deriving, type classes, IO and the ReaderT app pattern, strictness and space leaks, aeson JSON, hspec and QuickCheck tests - and decodes common GHC type errors with typed holes and GHCi. Use when working in a Haskell codebase for the first time, fixing build or type errors, adding a feature to a cabal or Stack project, or writing small production tools in Haskell.

Use it when

  • Working in an unfamiliar cabal or Stack project
  • GHC type errors or build failures need decoding
  • Adding features, tests or JSON handling to Haskell code
  • Memory grows over time in a long-running Haskell program

Use something else when

  • The project is Scala or another JVM functional language: use scala-3
  • You want generic property-based testing guidance across languages: use property-based-testing

How to check the result

The build is -Wall clean, cabal test or stack test passes, and HLint plus the repo's formatter report no new issues on changed modules.

The mistake it prevents

Using foldl, lazy Map or lazy IORef updates on accumulating state, building thunks until memory runs out.

~/.agents/skills/haskell-basics/SKILL.md

--- name: haskell-basics description: Reads, writes and fixes practical Haskell with GHC, cabal or Stack - project layout, Text over String, Maybe and Either error handling, records and… license: MIT ---

Practical Haskell compiles with -Wall clean, keeps IO at the edges and logic pure, uses Text, strict maps and explicit error types, has no partial functions on input data, and is checked with tests and HLint. Type errors are resolved by reading what GHC expected versus what it got, not by adding annotations until it compiles.

Orient first

  1. Read AGENTS.md, *.cabal or package.yaml (hpack), cabal.project, stack.yaml, and .hlint.yaml / fourmolu.yaml.
  2. Tool: if stack.yaml exists use Stack (stack build, stack test); otherwise cabal (cabal build all, cabal test all). Do not mix them in one project.
  3. Note the GHC version (ghc --version, the Stack snapshot resolver/snapshot, or with-compiler in cabal.project) and the default-language (GHC2021, GHC2024 or Haskell2010 plus extensions).
  4. First builds download the package index and dependencies (and Stack may download GHC); that needs network access and can take a long time. Ask before running.
  5. Map the code: exposed-modules in the library stanza, main-is for executables, hs-source-dirs for where files live.

Daily commands

TaskcabalStack
Buildcabal build allstack build
Testcabal test all --test-show-details=directstack test
REPLcabal repl lib:mypkgstack ghci
Fast reload loopghcid --command "cabal repl"ghcid --command "stack ghci"
Runcabal run myexe -- argsstack exec myexe -- args
Pin depscabal freeze / index-state in cabal.projectsnapshot in stack.yaml

In GHCi: :t expr (type), :i Name (info and instances), :r (reload), :set -Wall.

Idioms

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DerivingStrategies, DeriveAnyClass, DeriveGeneric #-}
module Billing.Invoice
  ( Invoice (..)
  , InvoiceError (..)
  , mkInvoice
  , total
  ) where

import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)

data InvoiceError
  = EmptyCustomer
  | NegativeAmount Int
  deriving stock (Eq, Show)

data Invoice = Invoice
  { invoiceCustomer :: !Text
  , invoiceLines    :: ![Int]      -- amounts in cents
  }
  deriving stock (Eq, Show, Generic)
  deriving anyclass (FromJSON, ToJSON)

-- Smart constructor: the only way to build a valid Invoice from untrusted input
mkInvoice :: Text -> [Int] -> Either InvoiceError Invoice
mkInvoice customer amounts
  | T.null (T.strip customer) = Left EmptyCustomer
  | (bad : _) <- filter (< 0) amounts = Left (NegativeAmount bad)
  | otherwise = Right (Invoice customer amounts)

total :: Invoice -> Int
total = sum . invoiceLines
  • Export lists on every module; hide constructors (Invoice without (..)) when a smart constructor must be used.
  • Text for text, ByteString for bytes, String only for tiny or legacy APIs.
  • Data.Map.Strict and Data.IORef.modifyIORef' (with the prime), and strict fields (!) on records that accumulate, to avoid thunk build-up.
  • foldl', never foldl, for accumulating sums and maps.
  • Maybe when absence is normal, Either Err a when the caller needs to know why, exceptions only for truly exceptional IO failures.

Avoid partial functions

PartialTotal alternative
head xs, tail xspattern match, Data.List.NonEmpty, or listToMaybe
fromJust mmaybe, fromMaybe, or a case
read sreadMaybe s (Text.Read)
xs !! idrop i xs with a match, or Data.Vector.!?
Map.! kMap.lookup k

GHC 9.8+ warns on head/tail (-Wx-partial); treat those warnings as real.

IO structure

Keep a pure core and a thin IO shell. For applications with config, logging and DB handles, the ReaderT pattern is the common default:

data Env = Env { envLogger :: Text -> IO (), envDb :: Connection }

newtype App a = App { runApp :: ReaderT Env IO a }
  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader Env)

logInfo :: Text -> App ()
logInfo msg = do
  logger <- asks envLogger
  liftIO (logger msg)

Follow whatever effect style the codebase already uses (mtl classes, ReaderT, effectful, polysemy); do not introduce a second one.

Exceptions: use bracket / finally for resources, try @IOException (with TypeApplications, on by default in GHC2021) for expected IO failures, and do not catch SomeException without rethrowing async exceptions (use safe-exceptions or unliftio helpers if the project has them).

Reading GHC errors

MessageUsually means
Couldn't match expected type 'Text' with actual type '[Char]'missing OverloadedStrings, or use T.pack
Couldn't match type 'IO a' with 'a'used an IO action as a value; bind it with <- in do
No instance for (Show X) / (FromJSON X)add a deriving clause or write the instance
Ambiguous type variable a0GHC cannot pick a type (often read, show . read, numeric literals); annotate or use @Type
Couldn't match type 'a' with 'Int', 'a' is a rigid type variableyour signature promises any a but the code needs a specific type; fix the signature
Variable not in scopemissing import, or the module does not export it
parse error on inputindentation in do, where, case or let blocks

Use typed holes to let GHC tell you what fits: write _ (or _name) where you are stuck and read the “Found hole” message with the expected type and valid fits.

Testing

import Test.Hspec
import Test.QuickCheck

spec :: Spec
spec = describe "mkInvoice" $ do
  it "rejects an empty customer" $
    mkInvoice "  " [100] `shouldBe` Left EmptyCustomer
  it "total is the sum of non-negative lines" $
    property $ \(xs :: [NonNegative Int]) ->
      fmap total (mkInvoice "acme" (map getNonNegative xs))
        == Right (sum (map getNonNegative xs))

Property tests fit pure code well; use them for parsers (round-trip decode . encode == Just), and unit tests for specific edge cases.

Verification

  • Build with -Wall (in ghc-options); no new warnings. -Werror belongs in CI flags, not in the published .cabal file.
  • Tests pass (cabal test all or stack test).
  • hlint . reports no new hints on changed modules (or each ignored hint is justified).
  • Formatter the repo uses (fourmolu, ormolu, stylish-haskell) leaves no diff.
  • For long-running code, check memory with +RTS -s on a realistic input when folds or accumulators changed.

Gotchas

  • Lazy IO (readFile from Prelude) can close handles late or leak; use Data.Text.IO / ByteString strict reads, or streaming libraries for big files.
  • length on Text is O(n); T.length in loops adds up.
  • show on Text adds quotes; use the value directly or T.unpack.
  • Orphan instances (instance defined away from both the class and the type) cause conflicts; put instances with the type, or use a newtype.
  • Changing an exported type or adding a constructor is a breaking change under the PVP; bump the major version for libraries.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Practical Haskell"
  short_description: "Haskell that builds clean and has no partial functions"
  default_prompt: "$haskell-basics Explain this GHC type error and fix it properly"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem