Expand mini-Haskell projects (add three more)

This commit is contained in:
Hassan Abedi 2026-04-22 11:45:09 +02:00
parent 660ba99f17
commit d7dc17eac0
22 changed files with 722 additions and 0 deletions

25
08-haskell-adt/README.md Normal file
View File

@ -0,0 +1,25 @@
# 08-haskell-adt
This example shows intermediate Haskell data modeling with algebraic data types.
It includes:
- sum types for targets, modes, and output style,
- a record type for the full build plan,
- pattern matching in both parsing and description logic, and
- a test suite run by `nix flake check`.
Useful commands:
```bash
nix develop
cabal run
cabal run -- executable release quiet
cabal test
nix build
./result/bin/mini-plan executable release quiet
nix run . -- executable release quiet
nix flake check
```

View File

@ -0,0 +1,13 @@
module Main where
import MiniPlan.Build (describePlan, parsePlan)
import System.Environment (getArgs)
import System.Exit (die)
main :: IO ()
main = do
args <- getArgs
case parsePlan args of
Left err -> die err
Right plan -> putStrLn (describePlan plan)

27
08-haskell-adt/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
}

38
08-haskell-adt/flake.nix Normal file
View File

@ -0,0 +1,38 @@
{
# Builds a small Haskell project that focuses on algebraic data types,
# record syntax, and pattern matching.
description = "A Haskell project for algebraic data types and pattern matching";
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-plan" ./. { };
checkedProject = pkgs.haskell.lib.doCheck project;
in
{
packages.${system}.default = project;
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/mini-plan";
meta.description = "Run the algebraic data type 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-plan
version: 0.1.0.0
build-type: Simple
library
exposed-modules: MiniPlan.Build
hs-source-dirs: src
build-depends: base >=4.14 && <5
default-language: Haskell2010
executable mini-plan
main-is: Main.hs
hs-source-dirs: app
build-depends:
base >=4.14 && <5,
mini-plan
default-language: Haskell2010
test-suite mini-plan-test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends:
base >=4.14 && <5,
mini-plan
default-language: Haskell2010

View File

@ -0,0 +1,74 @@
module MiniPlan.Build where
data Target
= Library
| Executable
| TestSuite
deriving (Eq, Show)
data Mode
= Debug
| Release
deriving (Eq, Show)
data Output
= Quiet
| Verbose
deriving (Eq, Show)
data BuildPlan = BuildPlan
{ target :: Target
, mode :: Mode
, output :: Output
}
deriving (Eq, Show)
defaultPlan :: BuildPlan
defaultPlan = BuildPlan Library Debug Verbose
describePlan :: BuildPlan -> String
describePlan plan =
case plan of
BuildPlan Library Debug Verbose ->
"compile the library in debug mode with verbose logs"
BuildPlan Library Release _ ->
"compile the library in release mode"
BuildPlan Executable Debug _ ->
"build the executable in debug mode"
BuildPlan Executable Release Verbose ->
"build the executable in release mode with verbose logs"
BuildPlan Executable Release Quiet ->
"build the executable in release mode quietly"
BuildPlan TestSuite planMode planOutput ->
"run the test suite in " ++ modeLabel planMode ++ " mode" ++ outputSuffix planOutput
modeLabel :: Mode -> String
modeLabel Debug = "debug"
modeLabel Release = "release"
outputSuffix :: Output -> String
outputSuffix Quiet = " quietly"
outputSuffix Verbose = " with verbose logs"
parsePlan :: [String] -> Either String BuildPlan
parsePlan [] = Right defaultPlan
parsePlan [targetArg, modeArg, outputArg] =
BuildPlan <$> parseTarget targetArg <*> parseMode modeArg <*> parseOutput outputArg
parsePlan _ =
Left "expected either no arguments or: <library|executable|test> <debug|release> <quiet|verbose>"
parseTarget :: String -> Either String Target
parseTarget "library" = Right Library
parseTarget "executable" = Right Executable
parseTarget "test" = Right TestSuite
parseTarget other = Left ("unknown target: " ++ other)
parseMode :: String -> Either String Mode
parseMode "debug" = Right Debug
parseMode "release" = Right Release
parseMode other = Left ("unknown mode: " ++ other)
parseOutput :: String -> Either String Output
parseOutput "quiet" = Right Quiet
parseOutput "verbose" = Right Verbose
parseOutput other = Left ("unknown output: " ++ other)

View File

@ -0,0 +1,27 @@
module Main where
import MiniPlan.Build
( BuildPlan (BuildPlan)
, Mode (Release)
, Output (Quiet)
, Target (Executable)
, defaultPlan
, describePlan
, parsePlan
)
import System.Exit (die)
main :: IO ()
main =
case
( parsePlan []
, parsePlan ["executable", "release", "quiet"]
, describePlan (BuildPlan Executable Release Quiet)
) of
( Right parsedDefault
, Right parsedCustom
, "build the executable in release mode quietly"
) | parsedDefault == defaultPlan
&& parsedCustom == BuildPlan Executable Release Quiet ->
putStrLn "test passed"
_ -> die "unexpected plan parsing result"

View File

@ -0,0 +1,25 @@
# 09-haskell-newtype
This example shows intermediate Haskell domain modeling with `newtype` and smart constructors.
It includes:
- `newtype` wrappers for `UserName` and `Email`,
- smart constructors that validate raw input,
- an explicit `Either String` error path, and
- a test suite run by `nix flake check`.
Useful commands:
```bash
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,23 @@
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import MiniRegistration.Domain (mkRegistration, welcomeMessage)
import System.Environment (getArgs)
import System.Exit (die)
main :: IO ()
main = do
args <- getArgs
(name, email) <-
case args of
[nameArg, emailArg] -> pure (Text.pack nameArg, Text.pack emailArg)
[] -> pure ("learner", "learner@example.com")
_ -> die "expected either no arguments or: <user-name> <email>"
case mkRegistration name email of
Left err -> die err
Right registration -> Text.putStrLn (welcomeMessage registration)

27
09-haskell-newtype/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 newtypes, smart
# constructors, and validation with Either.
description = "A Haskell project for newtypes and smart constructors";
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-registration" ./. { };
checkedProject = pkgs.haskell.lib.doCheck project;
in
{
packages.${system}.default = project;
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/mini-registration";
meta.description = "Run the newtype and validation 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,31 @@
cabal-version: 2.4
name: mini-registration
version: 0.1.0.0
build-type: Simple
library
exposed-modules: MiniRegistration.Domain
hs-source-dirs: src
build-depends:
base >=4.14 && <5,
text
default-language: Haskell2010
executable mini-registration
main-is: Main.hs
hs-source-dirs: app
build-depends:
base >=4.14 && <5,
mini-registration,
text
default-language: Haskell2010
test-suite mini-registration-test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends:
base >=4.14 && <5,
mini-registration,
text
default-language: Haskell2010

View File

@ -0,0 +1,54 @@
{-# LANGUAGE OverloadedStrings #-}
module MiniRegistration.Domain where
import Data.Char (isAlphaNum)
import Data.Text (Text)
import qualified Data.Text as Text
newtype UserName = UserName
{ unUserName :: Text
}
deriving (Eq, Show)
newtype Email = Email
{ unEmail :: Text
}
deriving (Eq, Show)
data Registration = Registration
{ userName :: UserName
, email :: Email
}
deriving (Eq, Show)
mkUserName :: Text -> Either String UserName
mkUserName rawName
| Text.length trimmedName < 3 = Left "user name must be at least 3 characters long"
| not (Text.all validUserNameChar trimmedName) = Left "user name must be letters or digits"
| otherwise = Right (UserName trimmedName)
where
trimmedName = Text.strip rawName
validUserNameChar char = isAlphaNum char || char == '_'
mkEmail :: Text -> Either String Email
mkEmail rawEmail
| Text.count "@" trimmedEmail /= 1 = Left "email must contain exactly one @"
| Text.isSuffixOf "@" trimmedEmail = Left "email must contain a domain"
| not (Text.any (== '.') domainPart) = Left "email domain must contain a dot"
| otherwise = Right (Email trimmedEmail)
where
trimmedEmail = Text.strip rawEmail
domainPart = Text.dropWhile (/= '@') trimmedEmail
mkRegistration :: Text -> Text -> Either String Registration
mkRegistration rawName rawEmail =
Registration <$> mkUserName rawName <*> mkEmail rawEmail
welcomeMessage :: Registration -> Text
welcomeMessage registration =
"welcome, "
<> unUserName (userName registration)
<> " ("
<> unEmail (email registration)
<> ")"

View File

@ -0,0 +1,22 @@
{-# LANGUAGE OverloadedStrings #-}
module Main where
import MiniRegistration.Domain
( mkRegistration
, mkUserName
, welcomeMessage
)
import System.Exit (die)
main :: IO ()
main =
case
( mkUserName "ha"
, mkRegistration "learner_1" "learner@example.com"
) of
( Left _
, Right registration
) | welcomeMessage registration == "welcome, learner_1 (learner@example.com)" ->
putStrLn "test passed"
_ -> die "unexpected validation result"

View File

@ -0,0 +1,25 @@
# 10-haskell-effects
This example shows intermediate Haskell application structure with `ReaderT` and `Except`.
It includes:
- an environment record carried by `ReaderT`,
- an explicit domain error type carried by `Except`,
- `MonadReader` and `MonadError` constraints on library functions, and
- a test suite run by `nix flake check`.
Useful commands:
```bash
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,17 @@
module Main where
import MiniEffects.App (defaultEnv, renderError, runGreetingApp)
import System.Environment (getArgs)
import System.Exit (die)
main :: IO ()
main = do
args <- getArgs
let rawName =
case args of
[] -> "learner"
firstArg : _ -> firstArg
case runGreetingApp defaultEnv rawName of
Left appError -> die (renderError appError)
Right greeting -> putStrLn greeting

27
10-haskell-effects/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 ReaderT, Except, and
# mtl-style constraints for application logic.
description = "A Haskell project for ReaderT and Except";
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-effects" ./. { };
checkedProject = pkgs.haskell.lib.doCheck project;
in
{
packages.${system}.default = project;
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/mini-effects";
meta.description = "Run the ReaderT and Except 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,31 @@
cabal-version: 2.4
name: mini-effects
version: 0.1.0.0
build-type: Simple
library
exposed-modules: MiniEffects.App
hs-source-dirs: src
build-depends:
base >=4.14 && <5,
mtl
default-language: Haskell2010
executable mini-effects
main-is: Main.hs
hs-source-dirs: app
build-depends:
base >=4.14 && <5,
mini-effects,
mtl
default-language: Haskell2010
test-suite mini-effects-test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends:
base >=4.14 && <5,
mini-effects,
mtl
default-language: Haskell2010

View File

@ -0,0 +1,61 @@
{-# LANGUAGE FlexibleContexts #-}
module MiniEffects.App where
import Control.Monad.Except
( Except
, MonadError
, runExcept
, throwError
)
import Control.Monad.Reader
( MonadReader
, ReaderT
, asks
, runReaderT
)
import Data.Char (isSpace)
data Env = Env
{ greetingPrefix :: String
, maxNameLength :: Int
}
deriving (Eq, Show)
data AppError
= EmptyName
| NameTooLong Int
deriving (Eq, Show)
type App = ReaderT Env (Except AppError)
defaultEnv :: Env
defaultEnv = Env { greetingPrefix = "hello", maxNameLength = 12 }
trim :: String -> String
trim = dropWhile isSpace . reverse . dropWhile isSpace . reverse
validateName :: (MonadReader Env m, MonadError AppError m) => String -> m String
validateName rawName
| null cleanedName = throwError EmptyName
| otherwise = do
nameLimit <- asks maxNameLength
if length cleanedName > nameLimit then
throwError (NameTooLong nameLimit)
else
pure cleanedName
where
cleanedName = trim rawName
buildGreeting :: (MonadReader Env m, MonadError AppError m) => String -> m String
buildGreeting rawName = do
validName <- validateName rawName
prefix <- asks greetingPrefix
pure (prefix ++ ", " ++ validName)
runGreetingApp :: Env -> String -> Either AppError String
runGreetingApp env rawName = runExcept (runReaderT (buildGreeting rawName) env)
renderError :: AppError -> String
renderError EmptyName = "name cannot be empty"
renderError (NameTooLong limit) = "name must be at most " ++ show limit ++ " characters long"

View File

@ -0,0 +1,21 @@
module Main where
import MiniEffects.App
( AppError (EmptyName, NameTooLong)
, Env (Env)
, runGreetingApp
)
import System.Exit (die)
main :: IO ()
main =
case
( runGreetingApp (Env "hi" 8) "haskell"
, runGreetingApp (Env "hi" 8) " "
, runGreetingApp (Env "hi" 4) "reader"
) of
( Right "hi, haskell"
, Left EmptyName
, Left (NameTooLong 4)
) -> putStrLn "test passed"
_ -> die "unexpected application stack result"

51
notes/010-haskell-adts.md Normal file
View File

@ -0,0 +1,51 @@
# Haskell Algebraic Data Types
This note covers `08-haskell-adt/`, which models a build plan with sum types, a record type, and pattern matching.
---
## 1. Why This Example Matters
Haskell programs often start by turning vague strings into precise domain types.
This example does that with:
- `Target` as a sum type,
- `Mode` as a sum type,
- `Output` as a sum type, and
- `BuildPlan` as a product type with record fields.
That is one of the most important intermediate Haskell habits: model the domain first, then write functions over the constructors.
---
## 2. Pattern Matching in Two Places
The example uses pattern matching in both parsing and behavior:
- `parseTarget`, `parseMode`, and `parseOutput` turn strings into constructors, and
- `describePlan` matches on the `BuildPlan` value to decide what to print.
That shows two common styles:
- pattern matching on one constructor at a time in small helper functions, and
- pattern matching on a whole record value when several fields matter together.
---
## 3. Commands to Try
```bash
cd 08-haskell-adt
nix develop
cabal run
cabal run -- executable release quiet
cabal test
nix build
./result/bin/mini-plan executable release quiet
nix run . -- executable release quiet
nix flake check
```