Add more mini-Haskell projects (plus related note files)

This commit is contained in:
Hassan Abedi 2026-04-22 15:43:26 +02:00
parent 78a89e1c88
commit 615d54531d
19 changed files with 720 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# 11-haskell-typeclasses
This example shows intermediate Haskell abstraction with a custom type class and several instances.
It includes:
- a custom `Renderable` type class,
- instances for domain types and a report wrapper,
- a small CLI that uses one shared rendering interface, and
- a test suite run by `nix flake check`.
Useful commands:
```bash
nix develop
cabal run
cabal run -- production failed 2
cabal test
nix build
./result/bin/mini-render production failed 2
nix run . -- production failed 2
nix flake check
```

View File

@ -0,0 +1,16 @@
module Main where
import MiniRender.Report (parseDeployment, render, sampleReport)
import System.Environment (getArgs)
import System.Exit (die)
main :: IO ()
main = do
args <- getArgs
case args of
[] -> putStr (render sampleReport)
_ ->
case parseDeployment args of
Left err -> die err
Right deployment -> putStrLn (render deployment)

27
11-haskell-typeclasses/flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -0,0 +1,38 @@
{
# Builds a small Haskell project that focuses on a custom type class and
# several instances that share one rendering interface.
description = "A Haskell project for type classes and custom instances";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{ self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
inherit (pkgs) haskellPackages;
project = haskellPackages.callCabal2nix "mini-render" ./. { };
checkedProject = pkgs.haskell.lib.doCheck project;
in
{
packages.${system}.default = project;
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/mini-render";
meta.description = "Run the type class and custom instance example.";
};
devShells.${system}.default = pkgs.mkShell {
packages = [
haskellPackages.ghc
pkgs.cabal-install
pkgs.haskell-language-server
];
};
checks.${system}.test-suite = checkedProject;
};
}

View File

@ -0,0 +1,27 @@
cabal-version: 2.4
name: mini-render
version: 0.1.0.0
build-type: Simple
library
exposed-modules: MiniRender.Report
hs-source-dirs: src
build-depends: base >=4.14 && <5
default-language: Haskell2010
executable mini-render
main-is: Main.hs
hs-source-dirs: app
build-depends:
base >=4.14 && <5,
mini-render
default-language: Haskell2010
test-suite mini-render-test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends:
base >=4.14 && <5,
mini-render
default-language: Haskell2010

View File

@ -0,0 +1,73 @@
module MiniRender.Report where
data Environment
= Staging
| Production
deriving (Eq, Show)
data BuildState
= Queued
| Running
| Passed
| Failed Int
deriving (Eq, Show)
data Deployment = Deployment
{ environment :: Environment
, buildState :: BuildState
}
deriving (Eq, Show)
newtype DeploymentReport = DeploymentReport
{ deployments :: [Deployment]
}
deriving (Eq, Show)
class Renderable a where
render :: a -> String
instance Renderable Environment where
render Staging = "staging"
render Production = "production"
instance Renderable BuildState where
render Queued = "queued"
render Running = "running"
render Passed = "passed"
render (Failed retryCount) = "failed after " ++ show retryCount ++ " retries"
instance Renderable Deployment where
render deployment =
render (environment deployment) ++ ": " ++ render (buildState deployment)
instance Renderable DeploymentReport where
render report = unlines (map render (deployments report))
sampleReport :: DeploymentReport
sampleReport =
DeploymentReport
[ Deployment Staging Passed
, Deployment Production (Failed 2)
]
parseEnvironment :: String -> Either String Environment
parseEnvironment "staging" = Right Staging
parseEnvironment "production" = Right Production
parseEnvironment other = Left ("unknown environment: " ++ other)
parseBuildState :: [String] -> Either String BuildState
parseBuildState ["queued"] = Right Queued
parseBuildState ["running"] = Right Running
parseBuildState ["passed"] = Right Passed
parseBuildState ["failed", retryCount] =
case reads retryCount of
[(parsedRetryCount, "")] -> Right (Failed parsedRetryCount)
_ -> Left ("invalid retry count: " ++ retryCount)
parseBuildState _ =
Left "expected one of: queued | running | passed | failed <retry-count>"
parseDeployment :: [String] -> Either String Deployment
parseDeployment [] =
Left "expected: <staging|production> <queued|running|passed|failed> [retry-count]"
parseDeployment (environmentArg : buildStateArgs) =
Deployment <$> parseEnvironment environmentArg <*> parseBuildState buildStateArgs

View File

@ -0,0 +1,25 @@
module Main where
import MiniRender.Report
( BuildState (Failed, Passed)
, Deployment (Deployment)
, DeploymentReport (DeploymentReport)
, Environment (Production, Staging)
, parseDeployment
, render
, sampleReport
)
import System.Exit (die)
main :: IO ()
main =
case
( render (Deployment Production (Failed 2))
, parseDeployment ["staging", "passed"]
, lines (render sampleReport)
) of
( "production: failed after 2 retries"
, Right (Deployment Staging Passed)
, ["staging: passed", "production: failed after 2 retries"]
) -> putStrLn "test passed"
_ -> die "unexpected rendering result"

View File

@ -0,0 +1,25 @@
# 12-haskell-parser-combinators
This example shows intermediate Haskell parsing with Megaparsec and parser combinators.
It includes:
- a small command language for deploy instructions,
- parser combinators for sequencing, choice, repetition, and end-of-input,
- a CLI that parses and renders the parsed command, and
- a test suite run by `nix flake check`.
Useful commands:
```bash
nix develop
cabal run
cabal run -- deploy api production tags=blue,stable
cabal test
nix build
./result/bin/mini-parser deploy api production tags=blue,stable
nix run . -- deploy api production tags=blue,stable
nix flake check
```

View File

@ -0,0 +1,17 @@
module Main where
import MiniParser.Deploy (parseDeployCommand, renderCommand)
import System.Environment (getArgs)
import System.Exit (die)
main :: IO ()
main = do
args <- getArgs
let input =
case args of
[] -> "deploy api staging tags=learning,flakes"
_ -> unwords args
case parseDeployCommand input of
Left err -> die err
Right command -> putStrLn (renderCommand command)

27
12-haskell-parser-combinators/flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -0,0 +1,38 @@
{
# Builds a small Haskell project that focuses on parser combinators with
# Megaparsec and a tiny command language.
description = "A Haskell project for parser combinators";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{ self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
inherit (pkgs) haskellPackages;
project = haskellPackages.callCabal2nix "mini-parser" ./. { };
checkedProject = pkgs.haskell.lib.doCheck project;
in
{
packages.${system}.default = project;
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/mini-parser";
meta.description = "Run the parser combinator example.";
};
devShells.${system}.default = pkgs.mkShell {
packages = [
haskellPackages.ghc
pkgs.cabal-install
pkgs.haskell-language-server
];
};
checks.${system}.test-suite = checkedProject;
};
}

View File

@ -0,0 +1,30 @@
cabal-version: 2.4
name: mini-parser
version: 0.1.0.0
build-type: Simple
library
exposed-modules: MiniParser.Deploy
hs-source-dirs: src
build-depends:
base >=4.14 && <5,
megaparsec,
text
default-language: Haskell2010
executable mini-parser
main-is: Main.hs
hs-source-dirs: app
build-depends:
base >=4.14 && <5,
mini-parser
default-language: Haskell2010
test-suite mini-parser-test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends:
base >=4.14 && <5,
mini-parser
default-language: Haskell2010

View File

@ -0,0 +1,86 @@
module MiniParser.Deploy where
import Control.Monad (void)
import Data.Void (Void)
import Text.Megaparsec
( Parsec
, choice
, eof
, errorBundlePretty
, many
, parse
, sepBy1
, some
, (<|>)
)
import Text.Megaparsec.Char (alphaNumChar, char, space1, string)
data Environment
= Staging
| Production
deriving (Eq, Show)
data DeployCommand = DeployCommand
{ serviceName :: String
, environment :: Environment
, tags :: [String]
}
deriving (Eq, Show)
type Parser = Parsec Void String
environmentParser :: Parser Environment
environmentParser =
choice
[ Production <$ string "production"
, Staging <$ string "staging"
]
identifierParser :: Parser String
identifierParser = some (alphaNumChar <|> char '_' <|> char '-')
tagParser :: Parser [String]
tagParser = string "tags=" *> (identifierParser `sepBy1` char ',')
deployCommandParser :: Parser DeployCommand
deployCommandParser = do
void (string "deploy")
space1
parsedService <- identifierParser
space1
parsedEnvironment <- environmentParser
parsedTags <- many (space1 *> tagParser)
eof
pure
DeployCommand
{ serviceName = parsedService
, environment = parsedEnvironment
, tags = concat parsedTags
}
parseDeployCommand :: String -> Either String DeployCommand
parseDeployCommand input =
case parse deployCommandParser "deploy-command" input of
Left parseError -> Left (errorBundlePretty parseError)
Right command -> Right command
renderCommand :: DeployCommand -> String
renderCommand command =
"deploy "
++ serviceName command
++ " to "
++ renderEnvironment (environment command)
++ renderTags (tags command)
renderEnvironment :: Environment -> String
renderEnvironment Staging = "staging"
renderEnvironment Production = "production"
renderTags :: [String] -> String
renderTags [] = " with no tags"
renderTags parsedTags = " with tags: " ++ commaSeparated parsedTags
commaSeparated :: [String] -> String
commaSeparated [] = ""
commaSeparated [singleItem] = singleItem
commaSeparated (firstItem : remainingItems) = firstItem ++ ", " ++ commaSeparated remainingItems

View File

@ -0,0 +1,23 @@
module Main where
import MiniParser.Deploy
( DeployCommand (DeployCommand)
, Environment (Production, Staging)
, parseDeployCommand
, renderCommand
)
import System.Exit (die)
main :: IO ()
main =
case
( parseDeployCommand "deploy api production tags=blue,stable"
, parseDeployCommand "deploy worker staging"
, parseDeployCommand "bad input"
) of
( Right (DeployCommand "api" Production ["blue", "stable"])
, Right parsedWorker
, Left _
) | renderCommand parsedWorker == "deploy worker to staging with no tags" ->
putStrLn "test passed"
_ -> die "unexpected parser result"

View File

@ -0,0 +1,50 @@
# Haskell Newtypes and Smart Constructors
This note covers `09-haskell-newtype/`, which models validated user input with `newtype`, record fields, and smart constructors.
---
## 1. Why `newtype` Matters
Plain `Text` values do not tell you what they represent. A user name and an email address could both be `Text`, even though they mean different things.
This example wraps those concepts explicitly:
- `UserName`,
- `Email`, and
- `Registration`.
That lets the rest of the code depend on validated domain types instead of raw input.
---
## 2. Smart Constructors
The module exposes constructor functions like `mkUserName` and `mkEmail`, which return `Either String ...`.
That is the main idea:
- invalid input is rejected at the boundary,
- successful validation returns a domain type, and
- the rest of the program works with trusted values.
This is a common intermediate Haskell pattern because it pushes validation close to the edge of the program.
---
## 3. Commands to Try
```bash
cd 09-haskell-newtype
nix develop
cabal run
cabal run -- learner learner@example.com
cabal test
nix build
./result/bin/mini-registration learner learner@example.com
nix run . -- learner learner@example.com
nix flake check
```

View File

@ -0,0 +1,53 @@
# Haskell Effects with ReaderT and Except
This note covers `10-haskell-effects/`, which models application logic with an environment, explicit errors, and a small effect stack.
---
## 1. What the Stack Represents
The example uses:
- `ReaderT Env` for read-only configuration, and
- `Except AppError` for failures that belong to the domain.
That is an important intermediate step because it separates three things cleanly:
- configuration,
- business logic, and
- error handling.
---
## 2. Why the Functions Use Constraints
The library functions are written against `MonadReader Env` and `MonadError AppError` constraints rather than a concrete stack type.
That keeps the functions reusable. They say what capabilities they need, not exactly which monad stack must provide them.
The concrete stack still exists:
```haskell
type App = ReaderT Env (Except AppError)
```
But the function signatures stay more flexible and easier to test.
---
## 3. Commands to Try
```bash
cd 10-haskell-effects
nix develop
cabal run
cabal run -- haskell
cabal test
nix build
./result/bin/mini-effects haskell
nix run . -- haskell
nix flake check
```

View File

@ -0,0 +1,48 @@
# Haskell Type Classes and Custom Instances
This note covers `11-haskell-typeclasses/`, which defines a custom type class and several instances that share one rendering interface.
---
## 1. Why Type Classes Matter
Type classes let you describe a capability independently from any one data type.
This example introduces:
- `Renderable` as a capability,
- `Environment`, `BuildState`, and `Deployment` as domain types, and
- `DeploymentReport` as a wrapper type for a whole collection.
Each type gets its own `Renderable` instance, but all of them can be used through the same `render` function.
---
## 2. Why the Instances Are Separate
Each instance decides how that type should appear:
- `Environment` renders short environment names,
- `BuildState` renders status text, and
- `Deployment` combines those smaller renderings into one message.
That shows the core value of type classes: behavior is attached per type, while the calling code can stay generic.
---
## 3. Commands to Try
```bash
cd 11-haskell-typeclasses
nix develop
cabal run
cabal run -- production failed 2
cabal test
nix build
./result/bin/mini-render production failed 2
nix run . -- production failed 2
nix flake check
```

View File

@ -0,0 +1,42 @@
# Haskell Learning Path
This note links the Haskell examples in a suggested order from first project structure through intermediate language and application patterns.
---
## 1. Suggested Order
1. `05-haskell/`: a small Cabal library, executable, test suite, and dev shell
2. `06-haskell-shellfor/`: a Haskell package set override and a `shellFor`-based dev shell
3. `07-haskell-deps/`: external Haskell libraries through Cabal `build-depends`
4. `08-haskell-adt/`: algebraic data types, records, and pattern matching
5. `09-haskell-newtype/`: `newtype`, smart constructors, and validation
6. `10-haskell-effects/`: `ReaderT`, `Except`, and constrained application logic
7. `11-haskell-typeclasses/`: custom type classes and per-type instances
8. `12-haskell-parser-combinators/`: parser combinators with Megaparsec
---
## 2. What to Focus on at Each Step
- `05-haskell/`: how a Cabal package becomes a flake package, app, dev shell, and check
- `06-haskell-shellfor/`: when a Haskell-specific dev shell is more useful than a generic shell
- `07-haskell-deps/`: why Cabal stays the source of truth for package dependencies
- `08-haskell-adt/`: how to model a problem with constructors before writing behavior
- `09-haskell-newtype/`: how to move validation to the boundary and protect the domain model
- `10-haskell-effects/`: how to separate configuration, logic, and failures
- `11-haskell-typeclasses/`: how to abstract shared behavior across several types
- `12-haskell-parser-combinators/`: how to build a small language from reusable parser pieces
---
## 3. Related Notes
- `notes/007-haskell.md`
- `notes/008-haskell-shellfor.md`
- `notes/009-haskell-dependencies.md`
- `notes/010-haskell-adts.md`
- `notes/011-haskell-newtypes.md`
- `notes/012-haskell-effects.md`
- `notes/013-haskell-typeclasses.md`
- `notes/015-haskell-parser-combinators.md`

View File

@ -0,0 +1,50 @@
# Haskell Parser Combinators
This note covers `12-haskell-parser-combinators/`, which parses a tiny deploy-command language with Megaparsec.
---
## 1. Why Parser Combinators Matter
Parser combinators let you build a parser out of smaller parsers:
- parse one token,
- combine it with another parser,
- choose between alternatives, and
- repeat parts that can appear many times.
That makes them a very Haskell-shaped tool: you write small functions, compose them, and end up with a parser for a whole language.
---
## 2. What This Example Shows
The example defines parsers for:
- the environment,
- identifiers,
- tag lists, and
- the full deploy command.
The full parser then composes those pieces with sequencing, `choice`, `many`, and `sepBy1`.
That is the main intermediate idea: treat a parser as a reusable value, not a one-off block of string handling code.
---
## 3. Commands to Try
```bash
cd 12-haskell-parser-combinators
nix develop
cabal run
cabal run -- deploy api production tags=blue,stable
cabal test
nix build
./result/bin/mini-parser deploy api production tags=blue,stable
nix run . -- deploy api production tags=blue,stable
nix flake check
```