-
Notifications
You must be signed in to change notification settings - Fork 4
2. Functors, Applicatives, Readers, Traversals
Download English Pdf | Download French Pdf
French Section is below!
- Overview
- The Code (verbatim)
- Part A —
Either: parsing & validation - Part B —
Reader(aka(-> r)): Functor/Applicative/Monad in action - Part C —
traverse/sequence: flipping and collecting effects - Part D — Mix & Match:
Either+Reader+ rendering - Add-on 1 — Validation applicative that accumulates errors
- Add-on 2 —
ReaderT+Either: a tiny app stack - Add-on 3 — Functor law sanity checks (no QuickCheck needed)
- Add-on 4 —
Readerutilities:ask/asks/local - Add-on 5 —
traverse_implementations (recursive,foldr,foldMap) - Glossary of Terms
This tutorial walks through a single Haskell module that demonstrates:
-
Eitherfor error-aware computations, -
Readerfor environment-dependent computations (as plain functions and asReader/ReaderT), - Functors/Applicatives/Monads via concrete, readable examples,
-
Traversals (
traverse,sequence) to turn lists of effects into effectful lists, - A custom Validation applicative that accumulates errors (instead of failing fast),
- Multiple ways to implement
traverse_for sequencing effects while discarding results.
Everything is self-contained and compiles as one file.
{-# OPTIONS_GHC -Wall #-}
module Main where
import Control.Applicative (liftA2, (*>))
import Data.List.NonEmpty (NonEmpty(..))
import Text.Read (readMaybe)
-- Reader (plain) utilities
import Control.Monad.Reader (Reader, asks, local, runReader)
-- ReaderT transformer (qualified to avoid name clashes)
import qualified Control.Monad.Trans.Reader as RT
import Control.Monad.Trans.Class (lift)
import Data.Foldable (foldMap)
----------------------------------------------------------------
-- Part A — Either
----------------------------------------------------------------
-- (3) readPositive
readPositive :: String -> Either String Int
readPositive s =
case readMaybe s of
Nothing -> Left ("not an Int: " ++ s)
Just n -> if n > 0 then Right n else Left ("non-positive: " ++ s)
-- (4) mkRange
mkRange :: String -> String -> Either String (Int,Int)
mkRange loS hiS =
liftA2 (,) (readPositive loS) (readPositive hiS) >>= \(lo,hi) ->
if lo < hi then Right (lo,hi) else Left "invalid range: lo>=hi"
-- (5) parseAll
parseAll :: [String] -> Either String [Int]
parseAll = traverse readPositive
----------------------------------------------------------------
-- Part B — Reader (as (-> r))
----------------------------------------------------------------
-- (6) incEnv
incEnv :: Int -> Int
incEnv = fmap (+1) id
-- (7) Applicative combine: Env greeting (curried greet)
data Env = Env { firstName :: String, lastName :: String } deriving Show
greet :: String -> String -> String
greet first last = "Hi " ++ first ++ " " ++ last
fullGreeting :: Env -> String
fullGreeting = greet <$> firstName <*> lastName
-- (8) Reader monad composition
data Cfg = Cfg { base :: Int, factor :: Int } deriving Show
step1 :: Cfg -> Int
step1 = base
step2 :: Int -> Cfg -> Int
step2 x cfg = x * factor cfg
step3 :: Int -> Cfg -> String
step3 x _ = "result=" ++ show x
pipeline :: Cfg -> String
pipeline = step1 >>= step2 >>= step3 -- (-> Cfg) monad
-- (9) local environment tweak (pure functions)
priceWithTax :: Double -> (Double -> Double)
priceWithTax taxRate = \basePrice -> basePrice * (1 + taxRate)
total :: Double -> Double -> Double
total tax basePrice = priceWithTax tax basePrice
totalDiscounted :: Double -> Double -> Double
totalDiscounted tax basePrice = priceWithTax (tax * 0.9) basePrice
----------------------------------------------------------------
-- Part C — traverse / sequence
----------------------------------------------------------------
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)
----------------------------------------------------------------
-- Part D — Mix & Match
----------------------------------------------------------------
lookupKey :: String -> ([(String, Int)] -> Either String Int)
lookupKey k env =
case lookup k env of
Nothing -> Left ("missing: " ++ k)
Just v -> Right v
type Assoc = [(String, Int)]
need :: [String] -> (Assoc -> Either String [Int])
need ks env = traverse (\k -> lookupKey k env) ks
data C = C { low :: Int, high :: Int } deriving Show
mkC :: Int -> Int -> Either String C
mkC l h | l < h = Right (C l h)
| otherwise = Left "low>=high"
render :: C -> String
render c = "range: [" ++ show (low c) ++ ", " ++ show (high c) ++ ")"
build :: (Int, Int) -> (C -> String)
build (l,h) =
case mkC l h of
Left e -> const ("error: " ++ e)
Right c -> const (render c)
-- (20) traverse_ (custom)
traverse_ :: Applicative f => (a -> f b) -> [a] -> f ()
traverse_ _ [] = pure ()
traverse_ g (x:xs) = g x *> traverse_ g xs
----------------------------------------------------------------
-- Add-on 1: Validation style Applicative that accumulates errors
----------------------------------------------------------------
data V e a = Failure e | Success a
deriving (Show, Eq)
instance Functor (V e) where
fmap f (Success a) = Success (f a)
fmap _ (Failure e) = Failure e
instance Semigroup e => Applicative (V e) where
pure = Success
Success f <*> Success a = Success (f a)
Failure e1 <*> Failure e2 = Failure (e1 <> e2)
Failure e <*> _ = Failure e
_ <*> Failure e = Failure e
-- A helper to lift String messages into NonEmpty
one :: a -> NonEmpty a
one x = x :| []
readPositiveV :: String -> V (NonEmpty String) Int
readPositiveV s =
case readMaybe s of
Nothing -> Failure (one ("not an Int: " ++ s))
Just n -> if n > 0 then Success n else Failure (one ("non-positive: " ++ s))
-- Combine two validated fields, then check a cross-field invariant
mkRangeV :: String -> String -> V (NonEmpty String) (Int, Int)
mkRangeV loS hiS =
case liftA2 (,) (readPositiveV loS) (readPositiveV hiS) of
Failure es -> Failure es
Success (lo,hi) -> if lo < hi
then Success (lo,hi)
else Failure (one "invalid range: lo>=hi")
----------------------------------------------------------------
-- Add-on 2: ReaderT + Either composition
----------------------------------------------------------------
type App e r a = RT.ReaderT r (Either e) a
askAssoc :: App String Assoc Assoc
askAssoc = RT.ask
needKeyT :: String -> App String Assoc Int
needKeyT k = do
env <- RT.ask
case lookup k env of
Nothing -> lift (Left ("missing: " ++ k))
Just v -> pure v
needAllT :: [String] -> App String Assoc [Int]
needAllT = traverse needKeyT
----------------------------------------------------------------
-- Add-on 3: QuickCheck-style notes (kept simple & runnable without QuickCheck)
----------------------------------------------------------------
-- We show the Functor laws on a few concrete examples
functorIdTests :: IO ()
functorIdTests = do
putStrLn "Functor identity law (Maybe):"
print (fmap id (Just 5) == (Just 5))
print (fmap id (Nothing :: Maybe Int) == Nothing)
functorCompTests :: IO ()
functorCompTests = do
putStrLn "Functor composition law (Maybe):"
let f = (+1); g = (*2)
print (fmap (f . g) (Just 10) == (fmap f . fmap g) (Just 10))
print (fmap (f . g) (Nothing :: Maybe Int) == (fmap f . fmap g) (Nothing :: Maybe Int))
{-
-- If you want real QuickCheck, uncomment and add quickcheck to your build:
import Test.QuickCheck
prop_Functor_Id :: Maybe Int -> Bool
prop_Functor_Id x = fmap id x == x
prop_Functor_Comp :: Fun Int Int -> Fun Int Int -> Maybe Int -> Bool
prop_Functor_Comp (Fun _ f) (Fun _ g) x =
fmap (f . g) x == (fmap f . fmap g) x
-}
----------------------------------------------------------------
-- Add-on 4: Reader newtype demo with ask/asks/local
----------------------------------------------------------------
fullGreetingR :: Reader Env String
fullGreetingR = do
f <- asks firstName
l <- asks lastName
pure (greet f l)
promoGreeting :: Reader Env String
promoGreeting =
local (\e -> e { lastName = lastName e ++ " (VIP)" }) fullGreetingR
----------------------------------------------------------------
-- Add-on 5: traverse_ via foldMap (alternative implementation)
----------------------------------------------------------------
traverse_foldMap_ :: (Applicative f, Monoid (f ())) => (a -> f b) -> [a] -> f ()
traverse_foldMap_ g = foldMap (\x -> g x *> pure ())
----------------------------------------------------------------
-- Alternate styles (point-free / do-notation where it helps)
----------------------------------------------------------------
-- A3-alt) readPositive (point-free-ish helper)
readPositivePF :: String -> Either String Int
readPositivePF = maybeErr . readMaybe
where
maybeErr Nothing = Left "not an Int"
maybeErr (Just n) = if n > 0 then Right n else Left "non-positive"
-- A4-alt) mkRange using do-notation
mkRangeDo :: String -> String -> Either String (Int,Int)
mkRangeDo loS hiS = do
lo <- readPositive loS
hi <- readPositive hiS
if lo < hi then pure (lo,hi) else Left "invalid range: lo>=hi"
-- B7-alt) fullGreeting point-free (same shape as fullGreeting)
fullGreetingPF :: Env -> String
fullGreetingPF = greet <$> firstName <*> lastName
-- B8-alt) pipeline with explicit composition
pipelinePF :: Cfg -> String
pipelinePF cfg = step3 (step2 (step1 cfg) cfg) cfg
-- C12-alt) traverse with safeDiv (same but explicit)
safeDivs :: [Int] -> Maybe [Int]
safeDivs = traverse (safeDiv 100)
-- D15-alt) lookupKey using maybe
lookupKeyPF :: String -> (Assoc -> Either String Int)
lookupKeyPF k env = maybe (Left ("missing: " ++ k)) Right (lookup k env)
-- D16-alt) need (more point-free)
needPF :: [String] -> (Assoc -> Either String [Int])
needPF ks env = traverse (`lookupKey` env) ks
-- D20-alt) traverse_ using foldr
traverse_foldr :: Applicative f => (a -> f b) -> [a] -> f ()
traverse_foldr g = foldr (\x acc -> g x *> acc) (pure ())
----------------------------------------------------------------
-- Demo helpers
----------------------------------------------------------------
sep :: String -> IO ()
sep title = putStrLn ("\n--- " ++ title ++ " ---")
showEitherList :: Show a => Either String [a] -> String
showEitherList (Left e) = "Left " ++ show e
showEitherList (Right x) = "Right " ++ show x
main :: IO ()
main = do
sep "Part A — Either"
print (fmap (+1) (Right 4 :: Either String Int))
print (fmap (+1) (Left "err" :: Either String Int))
print (Right 3 >>= (\x -> Right (x*10) :: Either String Int))
print (Left "bad" >>= (\x -> Right (x*10) :: Either String Int))
print (liftA2 (+) (Left "A") (Left "B") :: Either String Int)
print (readPositive "10")
print (readPositive "0")
print (readPositive "abc")
print (mkRange "2" "5")
print (mkRange "5" "2")
putStrLn (showEitherList (parseAll ["3","2","x","5"]))
putStrLn (showEitherList (parseAll ["3","2","5"]))
sep "Part B — Reader"
print (incEnv 41) -- 42
print (fullGreeting (Env "Ada" "Lovelace"))
putStrLn (pipeline (Cfg 3 7))
print (total 0.15 100)
print (totalDiscounted 0.15 100)
sep "Part C — traverse / sequence"
print (sequence [Just 1, Just 2, Just 3])
print (sequence [Just 1, Nothing, Just 3])
print (traverse (safeDiv 100) [5,4,0,2])
sep "Part D — Mix & Match"
let env = [("a",10),("b",20)] :: Assoc
print (lookupKey "a" env)
print (lookupKey "c" env)
print (need ["a","b"] env)
print (need ["a","z"] env)
putStrLn (render (C 1 4))
putStrLn (build (1,4) (C 100 200))
putStrLn (build (4,1) (C 100 200))
sep "Add-on 1 — Validation (accumulating)"
print (readPositiveV "10")
print (readPositiveV "0")
print (mkRangeV "2" "5")
print (mkRangeV "0" "x") -- two errors accumulated
sep "Add-on 2 — ReaderT + Either"
let assoc = [("a",1),("b",2)] :: Assoc
print (RT.runReaderT (needAllT ["a","b"]) assoc)
print (RT.runReaderT (needAllT ["a","z"]) assoc)
sep "Add-on 3 — QuickCheck-style sanity checks"
functorIdTests
functorCompTests
sep "Add-on 4 — Reader ask/asks/local"
print (runReader fullGreetingR (Env "Grace" "Hopper"))
print (runReader promoGreeting (Env "Grace" "Hopper"))
sep "Add-on 5 — traverse_ variants"
print (traverse_ (\n -> if n>0 then Just () else Nothing) [1,2,3])
print (traverse_ (\n -> if n>0 then Just () else Nothing) [1,0,3])
print (traverse_foldMap_ (\n -> if n>0 then Just () else Nothing) [1,2,3])
print (traverse_foldr (\n -> if n>0 then Just () else Nothing) [1,2,3])
putStrLn "Done."-
readPositiveparsesString -> Either String Int. On bad parse, returnsLeft "not an Int: …". On non-positive, returnsLeft "non-positive: …". OtherwiseRight n. Takeaway:Either e amodels computations that can fail with an informative error. -
mkRangecomposes tworeadPositivecalls usingliftA2 (,)(Applicative) to build a pair(lo,hi), then uses>>=to checklo < hi. Pattern: Use Applicative to parse independently, then Monad to enforce relationships between parsed values. -
parseAll = traverse readPositivelifts element-wise parsing into list parsing: a single failure short-circuits toLeft.
-
incEnv = fmap (+1) idshowsFunctor (-> r): mapping over a function composes on its output. -
Env&greetdemonstrate Applicative for functions:-
fullGreeting = greet <$> firstName <*> lastNamemeans “call both getters on the sameEnv, then feed their results togreet.”
-
-
Cfgpipeline:-
pipeline = step1 >>= step2 >>= step3uses the Reader monad: each step receives the same environment (Cfg) automatically.
-
-
safeDivreturnsNothingon division by zero. -
traverse (safeDiv 100) [5,4,0,2]yieldsNothingat the first0. Rule: WithMaybe,traversefails fast; withEither, it returns the firstLeft.
-
lookupKey :: String -> (Assoc -> Either String Int)is a Reader of an association list that may fail (Either) if the key is missing. -
need :: [String] -> (Assoc -> Either String [Int])usestraverseto batch lookups: first missing key stops the process. -
C+mkC+render+build:-
mkCvalidateslow < high. -
buildturns a possibly failing construction into a total renderer function by capturing either an error message or a ready-to-userender. This cleanly separates validation from use.
-
-
V e a = Failure e | Success awithApplicativeinstance that combines errors via(<>)(requiresSemigroup e). - With
NonEmpty String,readPositiveVandmkRangeVcan collect multiple errors at once, e.g., both endpoints invalid.
-
type App e r a = ReaderT r (Either e) a: a tiny app monad with configuration (r) and fail-with-message (Either e). -
needKeyTreads the environment (RT.ask) and lifts failures vialift (Left ...). -
needAllTcomposes manyneedKeyToperations withtraverse. Pattern: A very common real-world stack.
-
functorIdTestschecksfmap id == idforMaybe. -
functorCompTestschecksfmap (f . g) == fmap f . fmap g. These are quick, executable sanity checks. (Commented QuickCheck props show how you’d property-test them.)
-
fullGreetingRusesasksto pull fields;promoGreetinguseslocalto temporarily tweak the environment (append “(VIP)” tolastName). Key idea:local= “run a computation under a modified environment.”
-
Recursive version:
traverse_ g (x:xs) = g x *> traverse_ g xs— sequences effects, discards values. -
foldrversion:traverse_foldr g = foldr (\x acc -> g x *> acc) (pure ())— same behavior via folding. -
foldMapversion:traverse_foldMap_ :: (Applicative f, Monoid (f ())) => (a -> f b) -> [a] -> f () traverse_foldMap_ g = foldMap (\x -> g x *> pure ())
Requires
Monoid (f ())becausefoldMapaggregates monoidal results.
-
Functor: Things you can map over.
fmap :: (a -> b) -> f a -> f b. -
Applicative: Functors that support function application in a context.
(<*>),pure, and helpers likeliftA2. Great for independent effects/validations. -
Monad: Applicatives that support dependent sequencing via
(>>=)(“bind”). -
Reader: A pattern for environment-passing computations. Plain functions
r -> aform a Reader functor/applicative/monad. -
ReaderT: Monad transformer adding a read-only environment to another effect (here,
Either e). -
Either e a: Error-aware computation.
Left e= error,Right a= success. -
Maybe a: Partial computation.
Nothing= failure,Just a= success. -
traverse: Map a function producing effects over a structure, then flip the structure and the effect:traverse :: Applicative f => (a -> f b) -> t a -> f (t b). -
sequence:sequence :: Applicative f => t (f a) -> f (t a); the special case oftraverse id. -
liftA2: Lift a binary function into an Applicative context, combining two effectful arguments. -
(<$>)/(<*>)/(*>): Infix aliases forfmap, applicative apply, and sequencing (discarding left value). -
asks/ask/local: Read parts of the environment; temporarily run under a modified environment. -
NonEmpty: A list guaranteed to have at least one element — perfect for aggregating one-or-more errors. -
Semigroup/Monoid: Types you can combine with(<>); Monoids also have an identitymempty. Used here to accumulate validation errors. -
foldMap: Map each element to a monoid and combine them — used for thefoldMap-basedtraverse_.
Super — voici la version française et son PDF. Le code ci-dessous est repris verbatim (inchangé), comme demandé.
- Vue d’ensemble
- Le code (verbatim – ne pas modifier)
- Partie A —
Either: analyse & validation - Partie B —
Reader(fonctions(-> r)) : Functor/Applicative/Monad - Partie C —
traverse/sequence: inversion structure/effet - Partie D — Mix & Match :
Either+Reader+ rendu - Module additionnel 1 — Applicative « Validation » (accumulation d’erreurs)
- Module additionnel 2 —
ReaderT+Either: mini pile applicative - Module additionnel 3 — Sanity checks des lois de foncteur
- Module additionnel 4 — Utilitaires
Reader:asks/local - Module additionnel 5 — Variantes de
traverse_ - Glossaire
Ce tutoriel montre, dans un seul module Haskell, comment :
- utiliser
Eitherpour parser et valider avec messages d’erreur (échec immédiat) ; - écrire du code dépendant d’un environnement avec des fonctions
(r -> a), puis avecReader/ReaderT; - mettre en pratique Functor / Applicative / Monad et savoir quand choisir l’un ou l’autre ;
- employer
traverse/sequencepour « retourner » structure et effet et agréger les résultats ; - construire une Applicative de Validation qui accumule plusieurs erreurs (au lieu de s’arrêter à la première).
{-# OPTIONS_GHC -Wall #-}
module Main where
import Control.Applicative (liftA2, (*>))
import Data.List.NonEmpty (NonEmpty(..))
import Text.Read (readMaybe)
-- Reader (plain) utilities
import Control.Monad.Reader (Reader, asks, local, runReader)
-- ReaderT transformer (qualified to avoid name clashes)
import qualified Control.Monad.Trans.Reader as RT
import Control.Monad.Trans.Class (lift)
import Data.Foldable (foldMap)
----------------------------------------------------------------
-- Part A — Either
----------------------------------------------------------------
-- (3) readPositive
readPositive :: String -> Either String Int
readPositive s =
case readMaybe s of
Nothing -> Left ("not an Int: " ++ s)
Just n -> if n > 0 then Right n else Left ("non-positive: " ++ s)
-- (4) mkRange
mkRange :: String -> String -> Either String (Int,Int)
mkRange loS hiS =
liftA2 (,) (readPositive loS) (readPositive hiS) >>= \(lo,hi) ->
if lo < hi then Right (lo,hi) else Left "invalid range: lo>=hi"
-- (5) parseAll
parseAll :: [String] -> Either String [Int]
parseAll = traverse readPositive
----------------------------------------------------------------
-- Part B — Reader (as (-> r))
----------------------------------------------------------------
-- (6) incEnv
incEnv :: Int -> Int
incEnv = fmap (+1) id
-- (7) Applicative combine: Env greeting (curried greet)
data Env = Env { firstName :: String, lastName :: String } deriving Show
greet :: String -> String -> String
greet first last = "Hi " ++ first ++ " " ++ last
fullGreeting :: Env -> String
fullGreeting = greet <$> firstName <*> lastName
-- (8) Reader monad composition
data Cfg = Cfg { base :: Int, factor :: Int } deriving Show
step1 :: Cfg -> Int
step1 = base
step2 :: Int -> Cfg -> Int
step2 x cfg = x * factor cfg
step3 :: Int -> Cfg -> String
step3 x _ = "result=" ++ show x
pipeline :: Cfg -> String
pipeline = step1 >>= step2 >>= step3 -- (-> Cfg) monad
-- (9) local environment tweak (pure functions)
priceWithTax :: Double -> (Double -> Double)
priceWithTax taxRate = \basePrice -> basePrice * (1 + taxRate)
total :: Double -> Double -> Double
total tax basePrice = priceWithTax tax basePrice
totalDiscounted :: Double -> Double -> Double
totalDiscounted tax basePrice = priceWithTax (tax * 0.9) basePrice
----------------------------------------------------------------
-- Part C — traverse / sequence
----------------------------------------------------------------
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)
----------------------------------------------------------------
-- Part D — Mix & Match
----------------------------------------------------------------
lookupKey :: String -> ([(String, Int)] -> Either String Int)
lookupKey k env =
case lookup k env of
Nothing -> Left ("missing: " ++ k)
Just v -> Right v
type Assoc = [(String, Int)]
need :: [String] -> (Assoc -> Either String [Int])
need ks env = traverse (\k -> lookupKey k env) ks
data C = C { low :: Int, high :: Int } deriving Show
mkC :: Int -> Int -> Either String C
mkC l h | l < h = Right (C l h)
| otherwise = Left "low>=high"
render :: C -> String
render c = "range: [" ++ show (low c) ++ ", " ++ show (high c) ++ ")"
build :: (Int, Int) -> (C -> String)
build (l,h) =
case mkC l h of
Left e -> const ("error: " ++ e)
Right c -> const (render c)
-- (20) traverse_ (custom)
traverse_ :: Applicative f => (a -> f b) -> [a] -> f ()
traverse_ _ [] = pure ()
traverse_ g (x:xs) = g x *> traverse_ g xs
----------------------------------------------------------------
-- Add-on 1: Validation style Applicative that accumulates errors
----------------------------------------------------------------
data V e a = Failure e | Success a
deriving (Show, Eq)
instance Functor (V e) where
fmap f (Success a) = Success (f a)
fmap _ (Failure e) = Failure e
instance Semigroup e => Applicative (V e) where
pure = Success
Success f <*> Success a = Success (f a)
Failure e1 <*> Failure e2 = Failure (e1 <> e2)
Failure e <*> _ = Failure e
_ <*> Failure e = Failure e
-- A helper to lift String messages into NonEmpty
one :: a -> NonEmpty a
one x = x :| []
readPositiveV :: String -> V (NonEmpty String) Int
readPositiveV s =
case readMaybe s of
Nothing -> Failure (one ("not an Int: " ++ s))
Just n -> if n > 0 then Success n else Failure (one ("non-positive: " ++ s))
-- Combine two validated fields, then check a cross-field invariant
mkRangeV :: String -> String -> V (NonEmpty String) (Int, Int)
mkRangeV loS hiS =
case liftA2 (,) (readPositiveV loS) (readPositiveV hiS) of
Failure es -> Failure es
Success (lo,hi) -> if lo < hi
then Success (lo,hi)
else Failure (one "invalid range: lo>=hi")
----------------------------------------------------------------
-- Add-on 2: ReaderT + Either composition
----------------------------------------------------------------
type App e r a = RT.ReaderT r (Either e) a
askAssoc :: App String Assoc Assoc
askAssoc = RT.ask
needKeyT :: String -> App String Assoc Int
needKeyT k = do
env <- RT.ask
case lookup k env of
Nothing -> lift (Left ("missing: " ++ k))
Just v -> pure v
needAllT :: [String] -> App String Assoc [Int]
needAllT = traverse needKeyT
----------------------------------------------------------------
-- Add-on 3: QuickCheck-style notes (kept simple & runnable without QuickCheck)
----------------------------------------------------------------
-- We show the Functor laws on a few concrete examples
functorIdTests :: IO ()
functorIdTests = do
putStrLn "Functor identity law (Maybe):"
print (fmap id (Just 5) == (Just 5))
print (fmap id (Nothing :: Maybe Int) == Nothing)
functorCompTests :: IO ()
functorCompTests = do
putStrLn "Functor composition law (Maybe):"
let f = (+1); g = (*2)
print (fmap (f . g) (Just 10) == (fmap f . fmap g) (Just 10))
print (fmap (f . g) (Nothing :: Maybe Int) == (fmap f . fmap g) (Nothing :: Maybe Int))
{-
-- If you want real QuickCheck, uncomment and add quickcheck to your build:
import Test.QuickCheck
prop_Functor_Id :: Maybe Int -> Bool
prop_Functor_Id x = fmap id x == x
prop_Functor_Comp :: Fun Int Int -> Fun Int Int -> Maybe Int -> Bool
prop_Functor_Comp (Fun _ f) (Fun _ g) x =
fmap (f . g) x == (fmap f . fmap g) x
-}
----------------------------------------------------------------
-- Add-on 4: Reader newtype demo with ask/asks/local
----------------------------------------------------------------
fullGreetingR :: Reader Env String
fullGreetingR = do
f <- asks firstName
l <- asks lastName
pure (greet f l)
promoGreeting :: Reader Env String
promoGreeting =
local (\e -> e { lastName = lastName e ++ " (VIP)" }) fullGreetingR
----------------------------------------------------------------
-- Add-on 5: traverse_ via foldMap (alternative implementation)
----------------------------------------------------------------
traverse_foldMap_ :: (Applicative f, Monoid (f ())) => (a -> f b) -> [a] -> f ()
traverse_foldMap_ g = foldMap (\x -> g x *> pure ())
----------------------------------------------------------------
-- Alternate styles (point-free / do-notation where it helps)
----------------------------------------------------------------
-- A3-alt) readPositive (point-free-ish helper)
readPositivePF :: String -> Either String Int
readPositivePF = maybeErr . readMaybe
where
maybeErr Nothing = Left "not an Int"
maybeErr (Just n) = if n > 0 then Right n else Left "non-positive"
-- A4-alt) mkRange using do-notation
mkRangeDo :: String -> String -> Either String (Int,Int)
mkRangeDo loS hiS = do
lo <- readPositive loS
hi <- readPositive hiS
if lo < hi then pure (lo,hi) else Left "invalid range: lo>=hi"
-- B7-alt) fullGreeting point-free (same shape as fullGreeting)
fullGreetingPF :: Env -> String
fullGreetingPF = greet <$> firstName <*> lastName
-- B8-alt) pipeline with explicit composition
pipelinePF :: Cfg -> String
pipelinePF cfg = step3 (step2 (step1 cfg) cfg) cfg
-- C12-alt) traverse with safeDiv (same but explicit)
safeDivs :: [Int] -> Maybe [Int]
safeDivs = traverse (safeDiv 100)
-- D15-alt) lookupKey using maybe
lookupKeyPF :: String -> (Assoc -> Either String Int)
lookupKeyPF k env = maybe (Left ("missing: " ++ k)) Right (lookup k env)
-- D16-alt) need (more point-free)
needPF :: [String] -> (Assoc -> Either String [Int])
needPF ks env = traverse (`lookupKey` env) ks
-- D20-alt) traverse_ using foldr
traverse_foldr :: Applicative f => (a -> f b) -> [a] -> f ()
traverse_foldr g = foldr (\x acc -> g x *> acc) (pure ())
----------------------------------------------------------------
-- Demo helpers
----------------------------------------------------------------
sep :: String -> IO ()
sep title = putStrLn ("\n--- " ++ title ++ " ---")
showEitherList :: Show a => Either String [a] -> String
showEitherList (Left e) = "Left " ++ show e
showEitherList (Right x) = "Right " ++ show x
main :: IO ()
main = do
sep "Part A — Either"
print (fmap (+1) (Right 4 :: Either String Int))
print (fmap (+1) (Left "err" :: Either String Int))
print (Right 3 >>= (\x -> Right (x*10) :: Either String Int))
print (Left "bad" >>= (\x -> Right (x*10) :: Either String Int))
print (liftA2 (+) (Left "A") (Left "B") :: Either String Int)
print (readPositive "10")
print (readPositive "0")
print (readPositive "abc")
print (mkRange "2" "5")
print (mkRange "5" "2")
putStrLn (showEitherList (parseAll ["3","2","x","5"]))
putStrLn (showEitherList (parseAll ["3","2","5"]))
sep "Part B — Reader"
print (incEnv 41) -- 42
print (fullGreeting (Env "Ada" "Lovelace"))
putStrLn (pipeline (Cfg 3 7))
print (total 0.15 100)
print (totalDiscounted 0.15 100)
sep "Part C — traverse / sequence"
print (sequence [Just 1, Just 2, Just 3])
print (sequence [Just 1, Nothing, Just 3])
print (traverse (safeDiv 100) [5,4,0,2])
sep "Part D — Mix & Match"
let env = [("a",10),("b",20)] :: Assoc
print (lookupKey "a" env)
print (lookupKey "c" env)
print (need ["a","b"] env)
print (need ["a","z"] env)
putStrLn (render (C 1 4))
putStrLn (build (1,4) (C 100 200))
putStrLn (build (4,1) (C 100 200))
sep "Add-on 1 — Validation (accumulating)"
print (readPositiveV "10")
print (readPositiveV "0")
print (mkRangeV "2" "5")
print (mkRangeV "0" "x") -- two errors accumulated
sep "Add-on 2 — ReaderT + Either"
let assoc = [("a",1),("b",2)] :: Assoc
print (RT.runReaderT (needAllT ["a","b"]) assoc)
print (RT.runReaderT (needAllT ["a","z"]) assoc)
sep "Add-on 3 — QuickCheck-style sanity checks"
functorIdTests
functorCompTests
sep "Add-on 4 — Reader ask/asks/local"
print (runReader fullGreetingR (Env "Grace" "Hopper"))
print (runReader promoGreeting (Env "Grace" "Hopper"))
sep "Add-on 5 — traverse_ variants"
print (traverse_ (\n -> if n>0 then Just () else Nothing) [1,2,3])
print (traverse_ (\n -> if n>0 then Just () else Nothing) [1,0,3])
print (traverse_foldMap_ (\n -> if n>0 then Just () else Nothing) [1,2,3])
print (traverse_foldr (\n -> if n>0 then Just () else Nothing) [1,2,3])
putStrLn "Done."-
readPositive:String -> Either String Int. En cas d’échec de parse →Left, en cas d’entier ≤ 0 →Left, sinonRight n. -
mkRange:liftA2 (,)parse indépendamment les deux bornes ; ensuite>>=vérifielo < hi. -
parseAll = traverse readPositive: remonte la première erreur sur une liste.
-
incEnv = fmap (+1) id:fmapsur(-> r)compose la fonction de sortie. -
fullGreeting = greet <$> firstName <*> lastName: applique le mêmeEnvaux deux getters et les donne àgreet. -
pipeline = step1 >>= step2 >>= step3: le Reader monad file le mêmeCfg.
-
safeDivrenvoieNothingsur division par zéro. -
traverse (safeDiv 100) [5,4,0,2]→Nothingau premier0. AvecEither, on récupère le premierLeft.
-
lookupKeylit dans l’environnement (Assoc) et peut échouer (Left). -
needregroupe plusieurs lectures avectraverse. -
mkC+render+buildséparent validation (qui peut échouer) et affichage pur.
-
V e acumule les erreurs via(<>)(nécessiteSemigroup e, iciNonEmpty String). -
mkRangeVpeut renvoyer plusieurs messages d’erreur.
-
type App e r a = ReaderT r (Either e) a: configuration + erreurs. -
needAllTcompose des lectures dépendantes viatraverse.
- Vérifie
fmap id == idet la composition surMaybe(exécutables sans QuickCheck).
-
asksextrait un champ ;localexécute sous un env temporairement modifié.
-
Récursif et
foldr: nécessitent seulementApplicative. -
foldMap: demandeMonoid (f ())carfoldMapagrège par monoïde.
-
Functor : supporte
fmap :: (a -> b) -> f a -> f b. -
Applicative : applique des fonctions contextuelles (
<*>,liftA2). -
Monad : chaînage dépendant via
(>>=). -
Reader : passage d’environnement avec des fonctions
r -> a. - ReaderT : ajoute un environnement à un autre effet.
-
Either :
Left e(erreur) /Right a(succès). -
Maybe :
Nothing(échec) /Just a(succès). - traverse : map + inversion effet/structure.
-
sequence : inversion
t (f a)→f (t a). - NonEmpty : liste non vide (pratique pour ≥ 1 erreur).
-
Semigroup / Monoid : combinaison par
(<>);Monoidamempty.
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: