Add more mini-Haskell projects (plus related note files)
This commit is contained in:
parent
d7dc17eac0
commit
4177cdb552
25
11-haskell-typeclasses/README.md
Normal file
25
11-haskell-typeclasses/README.md
Normal 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
|
||||||
|
```
|
||||||
16
11-haskell-typeclasses/app/Main.hs
Normal file
16
11-haskell-typeclasses/app/Main.hs
Normal 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
27
11-haskell-typeclasses/flake.lock
generated
Normal 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
|
||||||
|
}
|
||||||
38
11-haskell-typeclasses/flake.nix
Normal file
38
11-haskell-typeclasses/flake.nix
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
27
11-haskell-typeclasses/mini-render.cabal
Normal file
27
11-haskell-typeclasses/mini-render.cabal
Normal 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
|
||||||
73
11-haskell-typeclasses/src/MiniRender/Report.hs
Normal file
73
11-haskell-typeclasses/src/MiniRender/Report.hs
Normal 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
|
||||||
25
11-haskell-typeclasses/test/Main.hs
Normal file
25
11-haskell-typeclasses/test/Main.hs
Normal 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"
|
||||||
25
12-haskell-parser-combinators/README.md
Normal file
25
12-haskell-parser-combinators/README.md
Normal 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
|
||||||
|
```
|
||||||
17
12-haskell-parser-combinators/app/Main.hs
Normal file
17
12-haskell-parser-combinators/app/Main.hs
Normal 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
27
12-haskell-parser-combinators/flake.lock
generated
Normal 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
|
||||||
|
}
|
||||||
38
12-haskell-parser-combinators/flake.nix
Normal file
38
12-haskell-parser-combinators/flake.nix
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
30
12-haskell-parser-combinators/mini-parser.cabal
Normal file
30
12-haskell-parser-combinators/mini-parser.cabal
Normal 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
|
||||||
86
12-haskell-parser-combinators/src/MiniParser/Deploy.hs
Normal file
86
12-haskell-parser-combinators/src/MiniParser/Deploy.hs
Normal 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
|
||||||
23
12-haskell-parser-combinators/test/Main.hs
Normal file
23
12-haskell-parser-combinators/test/Main.hs
Normal 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"
|
||||||
51
notes/011-haskell-newtypes.md
Normal file
51
notes/011-haskell-newtypes.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
53
notes/012-haskell-effects.md
Normal file
53
notes/012-haskell-effects.md
Normal 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
|
||||||
|
```
|
||||||
48
notes/013-haskell-typeclasses.md
Normal file
48
notes/013-haskell-typeclasses.md
Normal 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
|
||||||
|
```
|
||||||
42
notes/014-haskell-learning-path.md
Normal file
42
notes/014-haskell-learning-path.md
Normal 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`
|
||||||
50
notes/015-haskell-parser-combinators.md
Normal file
50
notes/015-haskell-parser-combinators.md
Normal 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
|
||||||
|
```
|
||||||
Loading…
x
Reference in New Issue
Block a user