From 72f23438b182af42a0f97bbd03ff4f0ead9e37ab Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 14:19:00 +0200 Subject: [PATCH] Add a more advanced haskell project --- 06-haskell-shellfor/README.md | 23 ++++++++ 07-haskell-deps/README.md | 25 +++++++++ 07-haskell-deps/app/Main.hs | 18 +++++++ 07-haskell-deps/flake.lock | 27 ++++++++++ 07-haskell-deps/flake.nix | 40 ++++++++++++++ 07-haskell-deps/mini-json.cabal | 33 ++++++++++++ 07-haskell-deps/src/MiniJson/Greeting.hs | 20 +++++++ 07-haskell-deps/test/Main.hs | 14 +++++ notes/009-haskell-dependencies.md | 67 ++++++++++++++++++++++++ 9 files changed, 267 insertions(+) create mode 100644 06-haskell-shellfor/README.md create mode 100644 07-haskell-deps/README.md create mode 100644 07-haskell-deps/app/Main.hs create mode 100644 07-haskell-deps/flake.lock create mode 100644 07-haskell-deps/flake.nix create mode 100644 07-haskell-deps/mini-json.cabal create mode 100644 07-haskell-deps/src/MiniJson/Greeting.hs create mode 100644 07-haskell-deps/test/Main.hs create mode 100644 notes/009-haskell-dependencies.md diff --git a/06-haskell-shellfor/README.md b/06-haskell-shellfor/README.md new file mode 100644 index 0000000..53d5b4c --- /dev/null +++ b/06-haskell-shellfor/README.md @@ -0,0 +1,23 @@ +# 06-haskell-shellfor + +This example shows a Haskell dev shell built with `shellFor`. + +It includes: + +- a local package added to the Haskell package set, +- a dev shell derived from that package with `shellFor`, and +- a small test suite run by `nix flake check`. + +Useful commands: + +```bash +nix develop +cabal run +cabal test + +nix build +./result/bin/mini-shellfor + +nix run +nix flake check +``` diff --git a/07-haskell-deps/README.md b/07-haskell-deps/README.md new file mode 100644 index 0000000..1da3151 --- /dev/null +++ b/07-haskell-deps/README.md @@ -0,0 +1,25 @@ +# 07-haskell-deps + +This example shows a small Haskell project that uses external libraries from the package set. + +It includes: + +- a library that parses JSON with `aeson`, +- `text` and `bytestring` usage in the library and CLI, +- an executable under `app/`, and +- a test suite run by `nix flake check`. + +Useful commands: + +```bash +nix develop +cabal run +cabal run -- '{"name":"flakes"}' +cabal test + +nix build +./result/bin/mini-json '{"name":"flakes"}' + +nix run . -- '{"name":"flakes"}' +nix flake check +``` diff --git a/07-haskell-deps/app/Main.hs b/07-haskell-deps/app/Main.hs new file mode 100644 index 0000000..6c6382e --- /dev/null +++ b/07-haskell-deps/app/Main.hs @@ -0,0 +1,18 @@ +module Main where + +import qualified Data.ByteString.Char8 as ByteString +import MiniJson.Greeting (greetFromJson) +import System.Environment (getArgs) +import System.Exit (die) + +main :: IO () +main = do + args <- getArgs + let input = + case args of + [] -> "{\"name\":\"learner\"}" + firstArg : _ -> firstArg + + case greetFromJson (ByteString.pack input) of + Left err -> die err + Right message -> putStrLn message diff --git a/07-haskell-deps/flake.lock b/07-haskell-deps/flake.lock new file mode 100644 index 0000000..dfdfdf9 --- /dev/null +++ b/07-haskell-deps/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/07-haskell-deps/flake.nix b/07-haskell-deps/flake.nix new file mode 100644 index 0000000..5a47f8a --- /dev/null +++ b/07-haskell-deps/flake.nix @@ -0,0 +1,40 @@ +{ + # Builds a small Haskell program that uses external libraries from the + # package set, so the example shows how Cabal dependencies flow through Nix. + description = "A Haskell project with external dependencies"; + + 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-json" ./. { }; + checkedProject = pkgs.haskell.lib.doCheck project; + in + { + packages.${system}.default = project; + + apps.${system}.default = { + type = "app"; + program = "${self.packages.${system}.default}/bin/mini-json"; + meta.description = "Run the Haskell JSON parsing example."; + }; + + devShells.${system}.default = pkgs.mkShell { + packages = [ + haskellPackages.ghc + pkgs.cabal-install + pkgs.haskell-language-server + ]; + }; + + # `doCheck` runs the Cabal test suite, which exercises both valid and + # invalid JSON input through the library function. + checks.${system}.test-suite = checkedProject; + }; +} diff --git a/07-haskell-deps/mini-json.cabal b/07-haskell-deps/mini-json.cabal new file mode 100644 index 0000000..e1c3832 --- /dev/null +++ b/07-haskell-deps/mini-json.cabal @@ -0,0 +1,33 @@ +cabal-version: 2.4 +name: mini-json +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: MiniJson.Greeting + hs-source-dirs: src + build-depends: + aeson, + base >=4.14 && <5, + bytestring, + text + default-language: Haskell2010 + +executable mini-json + main-is: Main.hs + hs-source-dirs: app + build-depends: + base >=4.14 && <5, + bytestring, + mini-json + default-language: Haskell2010 + +test-suite mini-json-test + type: exitcode-stdio-1.0 + main-is: Main.hs + hs-source-dirs: test + build-depends: + base >=4.14 && <5, + bytestring, + mini-json + default-language: Haskell2010 diff --git a/07-haskell-deps/src/MiniJson/Greeting.hs b/07-haskell-deps/src/MiniJson/Greeting.hs new file mode 100644 index 0000000..54c3ad7 --- /dev/null +++ b/07-haskell-deps/src/MiniJson/Greeting.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE OverloadedStrings #-} + +module MiniJson.Greeting where + +import Data.Aeson ((.:), FromJSON (parseJSON), eitherDecodeStrict', withObject) +import Data.ByteString (ByteString) +import Data.Text (Text) +import qualified Data.Text as Text + +data GreetingRequest = GreetingRequest + { name :: Text + } + +instance FromJSON GreetingRequest where + parseJSON = withObject "GreetingRequest" (\object -> GreetingRequest <$> object .: "name") + +greetFromJson :: ByteString -> Either String String +greetFromJson input = do + request <- eitherDecodeStrict' input + pure ("hello, " ++ Text.unpack (name request) ++ ", from aeson") diff --git a/07-haskell-deps/test/Main.hs b/07-haskell-deps/test/Main.hs new file mode 100644 index 0000000..03ade89 --- /dev/null +++ b/07-haskell-deps/test/Main.hs @@ -0,0 +1,14 @@ +module Main where + +import qualified Data.ByteString.Char8 as ByteString +import MiniJson.Greeting (greetFromJson) +import System.Exit (die) + +main :: IO () +main = + case + ( greetFromJson (ByteString.pack "{\"name\":\"flakes\"}") + , greetFromJson (ByteString.pack "{\"missing\":\"name\"}") + ) of + (Right "hello, flakes, from aeson", Left _) -> putStrLn "test passed" + _ -> die "unexpected parser result" diff --git a/notes/009-haskell-dependencies.md b/notes/009-haskell-dependencies.md new file mode 100644 index 0000000..3788909 --- /dev/null +++ b/notes/009-haskell-dependencies.md @@ -0,0 +1,67 @@ +# Haskell Dependencies + +This note covers `07-haskell-deps/`, which builds a small Haskell program that depends on external libraries from the Haskell package set. + +--- + +## 1. What This Example Adds + +The earlier Haskell examples stayed close to `base` and local modules. + +This example adds three common Haskell libraries: + +- `aeson` for JSON decoding, +- `text` for string data in the parsed value, and +- `bytestring` for the raw input passed into the decoder. + +That makes the example useful for two reasons: + +- it shows how Cabal `build-depends` entries become Nix build inputs automatically, and +- it demonstrates a more realistic program shape than a pure string concatenation example. + +--- + +## 2. The Important Point About Dependencies + +The flake still uses the same Nix expression pattern as `05-haskell/`: + +```nix +project = haskellPackages.callCabal2nix "mini-json" ./. { }; +``` + +The external libraries are not listed again in `flake.nix`. They are declared in `mini-json.cabal`, and `callCabal2nix` translates that Cabal package description into a Nix derivation. + +That is the main teaching point: Cabal remains the source of truth for Haskell package dependencies, while the flake decides how the package is exposed as a build, app, dev shell, and check. + +--- + +## 3. The Library Function + +The library exposes one function: + +```haskell +greetFromJson :: ByteString -> Either String String +``` + +It decodes a JSON object with a `name` field and returns a greeting string. The executable wraps that function in a small CLI, and the test suite checks both a valid input and an invalid one. + +That keeps the example focused on dependency usage, not CLI design. + +--- + +## 4. Commands to Try + +```bash +cd 07-haskell-deps + +nix develop +cabal run +cabal run -- '{"name":"flakes"}' +cabal test + +nix build +./result/bin/mini-json '{"name":"flakes"}' + +nix run . -- '{"name":"flakes"}' +nix flake check +```