Expand mini-Haskell projects (add three more)
This commit is contained in:
parent
abb13bce10
commit
78a89e1c88
25
08-haskell-adt/README.md
Normal file
25
08-haskell-adt/README.md
Normal 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
|
||||
```
|
||||
13
08-haskell-adt/app/Main.hs
Normal file
13
08-haskell-adt/app/Main.hs
Normal 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
27
08-haskell-adt/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
08-haskell-adt/flake.nix
Normal file
38
08-haskell-adt/flake.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
27
08-haskell-adt/mini-plan.cabal
Normal file
27
08-haskell-adt/mini-plan.cabal
Normal 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
|
||||
74
08-haskell-adt/src/MiniPlan/Build.hs
Normal file
74
08-haskell-adt/src/MiniPlan/Build.hs
Normal 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)
|
||||
27
08-haskell-adt/test/Main.hs
Normal file
27
08-haskell-adt/test/Main.hs
Normal 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"
|
||||
25
09-haskell-newtype/README.md
Normal file
25
09-haskell-newtype/README.md
Normal 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
|
||||
```
|
||||
23
09-haskell-newtype/app/Main.hs
Normal file
23
09-haskell-newtype/app/Main.hs
Normal 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
27
09-haskell-newtype/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
09-haskell-newtype/flake.nix
Normal file
38
09-haskell-newtype/flake.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
31
09-haskell-newtype/mini-registration.cabal
Normal file
31
09-haskell-newtype/mini-registration.cabal
Normal 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
|
||||
54
09-haskell-newtype/src/MiniRegistration/Domain.hs
Normal file
54
09-haskell-newtype/src/MiniRegistration/Domain.hs
Normal 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)
|
||||
<> ")"
|
||||
22
09-haskell-newtype/test/Main.hs
Normal file
22
09-haskell-newtype/test/Main.hs
Normal 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"
|
||||
25
10-haskell-effects/README.md
Normal file
25
10-haskell-effects/README.md
Normal 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
|
||||
```
|
||||
17
10-haskell-effects/app/Main.hs
Normal file
17
10-haskell-effects/app/Main.hs
Normal 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
27
10-haskell-effects/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
10-haskell-effects/flake.nix
Normal file
38
10-haskell-effects/flake.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
31
10-haskell-effects/mini-effects.cabal
Normal file
31
10-haskell-effects/mini-effects.cabal
Normal 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
|
||||
61
10-haskell-effects/src/MiniEffects/App.hs
Normal file
61
10-haskell-effects/src/MiniEffects/App.hs
Normal 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"
|
||||
21
10-haskell-effects/test/Main.hs
Normal file
21
10-haskell-effects/test/Main.hs
Normal 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
51
notes/010-haskell-adts.md
Normal 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
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user