diff --git a/11-haskell-typeclasses/README.md b/11-haskell-typeclasses/README.md new file mode 100644 index 0000000..5145001 --- /dev/null +++ b/11-haskell-typeclasses/README.md @@ -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 +``` diff --git a/11-haskell-typeclasses/app/Main.hs b/11-haskell-typeclasses/app/Main.hs new file mode 100644 index 0000000..ffb7222 --- /dev/null +++ b/11-haskell-typeclasses/app/Main.hs @@ -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) diff --git a/11-haskell-typeclasses/flake.lock b/11-haskell-typeclasses/flake.lock new file mode 100644 index 0000000..dfdfdf9 --- /dev/null +++ b/11-haskell-typeclasses/flake.lock @@ -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 +} diff --git a/11-haskell-typeclasses/flake.nix b/11-haskell-typeclasses/flake.nix new file mode 100644 index 0000000..c6d46d7 --- /dev/null +++ b/11-haskell-typeclasses/flake.nix @@ -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; + }; +} diff --git a/11-haskell-typeclasses/mini-render.cabal b/11-haskell-typeclasses/mini-render.cabal new file mode 100644 index 0000000..f88c622 --- /dev/null +++ b/11-haskell-typeclasses/mini-render.cabal @@ -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 diff --git a/11-haskell-typeclasses/src/MiniRender/Report.hs b/11-haskell-typeclasses/src/MiniRender/Report.hs new file mode 100644 index 0000000..bfdb2b0 --- /dev/null +++ b/11-haskell-typeclasses/src/MiniRender/Report.hs @@ -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 " + +parseDeployment :: [String] -> Either String Deployment +parseDeployment [] = + Left "expected: [retry-count]" +parseDeployment (environmentArg : buildStateArgs) = + Deployment <$> parseEnvironment environmentArg <*> parseBuildState buildStateArgs diff --git a/11-haskell-typeclasses/test/Main.hs b/11-haskell-typeclasses/test/Main.hs new file mode 100644 index 0000000..da93db0 --- /dev/null +++ b/11-haskell-typeclasses/test/Main.hs @@ -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" diff --git a/12-haskell-parser-combinators/README.md b/12-haskell-parser-combinators/README.md new file mode 100644 index 0000000..e827202 --- /dev/null +++ b/12-haskell-parser-combinators/README.md @@ -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 +``` diff --git a/12-haskell-parser-combinators/app/Main.hs b/12-haskell-parser-combinators/app/Main.hs new file mode 100644 index 0000000..9dfa2d9 --- /dev/null +++ b/12-haskell-parser-combinators/app/Main.hs @@ -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) diff --git a/12-haskell-parser-combinators/flake.lock b/12-haskell-parser-combinators/flake.lock new file mode 100644 index 0000000..dfdfdf9 --- /dev/null +++ b/12-haskell-parser-combinators/flake.lock @@ -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 +} diff --git a/12-haskell-parser-combinators/flake.nix b/12-haskell-parser-combinators/flake.nix new file mode 100644 index 0000000..bf0d6cc --- /dev/null +++ b/12-haskell-parser-combinators/flake.nix @@ -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; + }; +} diff --git a/12-haskell-parser-combinators/mini-parser.cabal b/12-haskell-parser-combinators/mini-parser.cabal new file mode 100644 index 0000000..20c7a4f --- /dev/null +++ b/12-haskell-parser-combinators/mini-parser.cabal @@ -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 diff --git a/12-haskell-parser-combinators/src/MiniParser/Deploy.hs b/12-haskell-parser-combinators/src/MiniParser/Deploy.hs new file mode 100644 index 0000000..cf1ba05 --- /dev/null +++ b/12-haskell-parser-combinators/src/MiniParser/Deploy.hs @@ -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 diff --git a/12-haskell-parser-combinators/test/Main.hs b/12-haskell-parser-combinators/test/Main.hs new file mode 100644 index 0000000..a31f863 --- /dev/null +++ b/12-haskell-parser-combinators/test/Main.hs @@ -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" diff --git a/notes/011-haskell-newtypes.md b/notes/011-haskell-newtypes.md new file mode 100644 index 0000000..ce1ff8f --- /dev/null +++ b/notes/011-haskell-newtypes.md @@ -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 +``` diff --git a/notes/012-haskell-effects.md b/notes/012-haskell-effects.md new file mode 100644 index 0000000..a1f1e41 --- /dev/null +++ b/notes/012-haskell-effects.md @@ -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 +``` diff --git a/notes/013-haskell-typeclasses.md b/notes/013-haskell-typeclasses.md new file mode 100644 index 0000000..175ea7d --- /dev/null +++ b/notes/013-haskell-typeclasses.md @@ -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 +``` diff --git a/notes/014-haskell-learning-path.md b/notes/014-haskell-learning-path.md new file mode 100644 index 0000000..3082de8 --- /dev/null +++ b/notes/014-haskell-learning-path.md @@ -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` diff --git a/notes/015-haskell-parser-combinators.md b/notes/015-haskell-parser-combinators.md new file mode 100644 index 0000000..630cd30 --- /dev/null +++ b/notes/015-haskell-parser-combinators.md @@ -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 +```