Oskar Wickström
Øredev, November 2018
data AccountBalance = AccountBalance Money Date
data Customer =
Customer
{ firstName :: Text
, lastName :: Text
}
data MealPreference
= Omnivore
| OvoLacto
| Vegetarian
fullName :: Customer -> Text
fullName customer =
firstName customer <> " " <> lastName customer
data Meal
= ChickenSandwich
| Omelette
| ChickpeaCurry
formatMeal :: Meal -> Text
formatMeal meal = case meal of
ChickenSandwich -> "Chicken Sandwich"
Omelette -> "Omelette"
ChickpeaCurry -> "Chickpea Curry"
data Order = Order { orderCustomer :: Customer
, orderMeal :: Meal
}
airlineStyleOrder :: Customer -> MealPreference -> Order
airlineStyleOrder customer pref =
case pref of
Omnivore -> Order customer ChickenSandwich
OvoLacto -> Order customer Omelette
Vegetarian -> Order customer ChickpeaCurry
printOrder :: Order -> IO ()
printOrder order =
let msg =
fullName (orderCustomer order)
<> " ordering "
<> formatMeal (orderMeal order)
<> "."
in Text.putStrLn msg
> let me = Customer "Oskar" "Wickström"
> let order = airlineStyleOrder me OvoLacto
> printOrder order
Oskar Wickström ordering Omelette.
class Functor f where
fmap :: (a -> b) -> f a -> f b
A Functor
instance for Maybe
:
data Maybe a = Just a | Nothing
instance Functor Maybe where
fmap f (Just a) = Just (f a)
fmap _ Nothing = Nothing
We can then fmap
functions over Maybe
values:
fmap (+1) (Just 10) -- Just 11
fmap (+1) Nothing -- Nothing
randomDouble :: IO Double -- between 0.0 and 1.0
randomLotteryPrize :: IO Integer
randomLotteryPrize =
fmap toAmount randomDouble
where
toAmount d = round (d * 1000000)
data Project
= SingleProject ProjectId
Text
| ProjectGroup Text
[Project]
deriving (Show, Eq)
data Budget = Budget
{ budgetIncome :: Money
, budgetExpenditure :: Money
} deriving (Show, Eq)
data Transaction
= Sale Money
| Purchase Money
deriving (Eq, Show)
data Report = Report
{ budgetProfit :: Money
, netProfit :: Money
, difference :: Money
} deriving (Show, Eq)
calculateReport :: Budget -> [Transaction] -> Report
calculateReport budget transactions = Report
{ budgetProfit = budgetProfit'
, netProfit = netProfit'
, difference = netProfit' - budgetProfit'
}
where
budgetProfit' = budgetIncome budget - budgetExpenditure budget
netProfit' = getSum (foldMap asProfit transactions)
asProfit (Sale m) = pure m
asProfit (Purchase m) = pure (negate m)
calculateReport :: Budget -> [Transaction] -> Report
calculateReport budget transactions = Report
{ budgetProfit = budgetProfit'
, netProfit = netProfit'
, difference = netProfit' - budgetProfit'
}
where
budgetProfit' = budgetIncome budget - budgetExpenditure budget
netProfit' = getSum (foldMap asProfit transactions)
asProfit (Sale m) = pure m
asProfit (Purchase m) = pure (negate m)
calculateReport :: Budget -> [Transaction] -> Report
calculateReport budget transactions = Report
{ budgetProfit = budgetProfit'
, netProfit = netProfit'
, difference = netProfit' - budgetProfit'
}
where
budgetProfit' = budgetIncome budget - budgetExpenditure budget
netProfit' = getSum (foldMap asProfit transactions)
asProfit (Sale m) = pure m
asProfit (Purchase m) = pure (negate m)
calculateReport :: Budget -> [Transaction] -> Report
calculateReport budget transactions = Report
{ budgetProfit = budgetProfit'
, netProfit = netProfit'
, difference = netProfit' - budgetProfit'
}
where
budgetProfit' = budgetIncome budget - budgetExpenditure budget
netProfit' = getSum (foldMap asProfit transactions)
asProfit (Sale m) = pure m
asProfit (Purchase m) = pure (negate m)
calculateReport :: Budget -> [Transaction] -> Report
calculateReport budget transactions = Report
{ budgetProfit = budgetProfit'
, netProfit = netProfit'
, difference = netProfit' - budgetProfit'
}
where
budgetProfit' = budgetIncome budget - budgetExpenditure budget
netProfit' = getSum (foldMap asProfit transactions)
asProfit (Sale m) = pure m
asProfit (Purchase m) = pure (negate m)
calculateProjectReport :: Project -> IO Report
calculateProjectReport project =
case project of
SingleProject p _ ->
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
ProjectGroup _ projects ->
foldMap calculateProjectReport projects
calculateProjectReport :: Project -> IO Report
calculateProjectReport project =
case project of
SingleProject p _ ->
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
ProjectGroup _ projects ->
foldMap calculateProjectReport projects
calculateProjectReport :: Project -> IO Report
calculateProjectReport project =
case project of
SingleProject p _ ->
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
ProjectGroup _ projects ->
foldMap calculateProjectReport projects
Semigroup (associative binary operation)
class Semigroup a where
(<>) :: a -> a -> a
Monoid (Semigroup with identity element)
class Semigroup a => Monoid a where
mempty :: a
instance Semigroup Report where
Report b1 n1 d1 <> Report b2 n2 d2 =
Report (b1 + b2) (n1 + n2) (d1 + d2)
instance Monoid Report where
mempty = Report 0 0 0
foldMap
:: (Foldable f, Monoid b)
=> (a -> b)
-> f a
-> b
foldMap
:: (Foldable f, Monoid b)
=> (a -> b)
-> f a
-> b
foldMap
:: (Foldable f, Monoid b)
=> (a -> b)
-> f a
-> b
asTree :: Project -> Tree String
asTree project =
case project of
SingleProject (ProjectId p) name ->
Node (printf "%s (%d)" name p) []
ProjectGroup name projects ->
Node (Text.unpack name) (map asTree projects)
prettyProject :: Project -> String
prettyProject = drawTree . asTree
asTree :: Project -> Tree String
asTree project =
case project of
SingleProject (ProjectId p) name ->
Node (printf "%s (%d)" name p) []
ProjectGroup name projects ->
Node (Text.unpack name) (map asTree projects)
prettyProject :: Project -> String
prettyProject = drawTree . asTree
asTree :: Project -> Tree String
asTree project =
case project of
SingleProject (ProjectId p) name ->
Node (printf "%s (%d)" name p) []
ProjectGroup name projects ->
Node (Text.unpack name) (map asTree projects)
prettyProject :: Project -> String
prettyProject = drawTree . asTree
someProject :: Project
someProject = ProjectGroup "Sweden" [stockholm, göteborg, malmö]
where
stockholm = SingleProject 1 "Stockholm"
göteborg = SingleProject 2 "Göteborg"
malmö = ProjectGroup "Malmö" [city, limhamn]
city = SingleProject 3 "Malmö City"
limhamn = SingleProject 4 "Limhamn"
> putStrLn (prettyProject someProject)
Sweden
|
+- Stockholm (1)
|
+- Göteborg (2)
|
`- Malmö
|
+- Malmö City (3)
|
`- Limhamn (4)
prettyReport :: Report -> String
prettyReport r =
printf
"Budget: %.2f, Net: %.2f, difference: %+.2f"
(unMoney (budgetProfit r))
(unMoney (netProfit r))
(unMoney (difference r))
> r <- calculateProjectReport someProject
> putStrLn (prettyReport r)
Budget: -14904.17, Net: 458.03, difference: +15362.20
data Project a
= SingleProject Text
a
| ProjectGroup Text
[Project a]
deriving (Show, Eq, Functor, Foldable, Traversable)
data Project a
= SingleProject Text
a
| ProjectGroup Text
[Project a]
deriving (Show, Eq, Functor, Foldable, Traversable)
traverse
:: (Traversable t, Applicative f)
=> (a -> f b)
-> t a
-> f (t b)
traverse
:: (Traversable t, Applicative f)
=> (a -> f b)
-> t a
-> f (t b)
traverse
:: (Traversable t, Applicative f)
=> (a -> f b)
-> t a
-> f (t b)
getDescription :: ProjectId -> IO Text
myProject :: Project ProjectId
example :: IO (Project Text)
example = traverse getDescription myProject
calculateProjectReports
:: Project ProjectId
-> IO (Project Report)
calculateProjectReports =
traverse $ \p ->
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
calculateProjectReports
:: Project ProjectId
-> IO (Project Report)
calculateProjectReports =
traverse $ \p ->
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
accumulateProjectReport :: Project Report -> Report
accumulateProjectReport = fold
asTree
:: (a -> String)
-> Project a
-> Tree String
prettyProject
:: (a -> String)
-> Project a
-> String
> pr <- calculateProjectReports someProject
> putStrLn (prettyProject prettyReport pr)
Sweden
|
+- Stockholm: Budget: -2259.99, Net: 391.23, difference: +2651.22
|
+- Göteborg: Budget: -3204.79, Net: -228.31, difference: +2976.48
|
`- Malmö
|
+- Malmö City: Budget: -6958.82, Net: 2811.88, difference: +9770.70
|
`- Limhamn: Budget: 5856.93, Net: 1941.43, difference: -3915.50
> putStrLn (prettyReport (accumulateProjectReport pr))
Budget: -6566.67, Net: 4916.23, difference: +11482.90
data Project g a
= SingleProject Text
a
| ProjectGroup Text
g
[Project g a]
deriving (Show, Eq, Functor, Foldable, Traversable)
calculateProjectReports
:: Project g ProjectId
-> IO (Project Report Report)
calculateProjectReports project =
fst <$> runWriterT (calc project)
where
-- ...
calculateProjectReports
:: Project g ProjectId
-> IO (Project Report Report)
calculateProjectReports project =
fst <$> runWriterT (calc project)
where
-- ...
calc (SingleProject name p) = do
report <- liftIO $
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
tell report
pure (SingleProject name report)
calc (SingleProject name p) = do
report <- liftIO $
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
tell report
pure (SingleProject name report)
calc (SingleProject name p) = do
report <- liftIO $
calculateReport
<$> DB.getBudget p
<*> DB.getTransactions p
tell report
pure (SingleProject name report)
calc (ProjectGroup name _ projects) = do
(projects', report) <- listen (traverse calc projects)
pure (ProjectGroup name report projects')
asTree
:: (g -> String)
-> (a -> String)
-> Project g a
-> Tree String
prettyProject
:: (g -> String)
-> (a -> String)
-> Project g a
-> String
> pr <- calculateProjectReports someProject
> putStrLn (prettyProject prettyReport prettyReport pr)
Sweden: Budget: -9278.10, Net: +4651.81, difference: +13929.91
|
+- Stockholm: Budget: -3313.83, Net: -805.37, difference: +2508.46
|
+- Göteborg: Budget: -422.48, Net: +1479.00, difference: +1901.48
|
`- Malmö: Budget: -5541.79, Net: +3978.18, difference: +9519.97
|
+- Malmö City: Budget: -4069.45, Net: +2185.02, difference: +6254.47
|
`- Limhamn: Budget: -1472.34, Net: +1793.16, difference: +3265.50
WriterT
monad transformerMonoid
Project
type has a hidden coupling to the reporting module
g
and a
parameters are only there for reporting