Skip to content

Instantly share code, notes, and snippets.

@lancelet
Created November 24, 2018 06:32
Show Gist options
  • Save lancelet/0ef0aae251f7831b23da7030ff2dbed3 to your computer and use it in GitHub Desktop.
Save lancelet/0ef0aae251f7831b23da7030ff2dbed3 to your computer and use it in GitHub Desktop.
FP vs OO; A Better Way to Count Candy
#!/usr/bin/env stack
{- stack
script
--resolver lts-12.19
--package containers
--package double-conversion
--package text
--package text-show
-}
{-
Haskell code from the blog post: "FP vs OO; A Better Way to Count Candy"
by Jonathan Merritt.
Install the Haskell Stack tool: https://docs.haskellstack.org/en/stable/README/
Run it as follows:
$ ./candy.hs
$ cat full_report.haskell.txt
JellyBeans: 44
JellyBabies: 18
Beans per Baby: 2.44
$ cat summary.haskell.txt
Beans per Baby: 2.44
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
import Data.Double.Conversion.Text (toFixed)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.IO (writeFile)
import System.IO (FilePath)
import TextShow (showt)
import Prelude hiding (writeFile)
newtype CandyBag = CandyBag (Map Text Int)
candyBags :: [CandyBag]
candyBags =
[ CandyBag (Map.fromList
[ ("jellybeans", 42)
, ("jellybabies", 10)
, ("frogs", 8) ])
, CandyBag (Map.fromList
[ ("jellybeans", 2)
, ("jellybabies", 8)
, ("nerds", 11) ])
]
data JellyCount
= JellyCount
{ beans :: Int
, babies :: Int
}
instance Semigroup JellyCount where
-- how to we smash two together?
JellyCount a b <> JellyCount c d = JellyCount (a + c) (b + d)
instance Monoid JellyCount where
-- how do we get an empty JellyCount?
mempty = JellyCount 0 0
countJelly :: CandyBag -> JellyCount
countJelly (CandyBag m) = JellyCount beans babies
where
beans = Map.findWithDefault 0 "jellybeans" m
babies = Map.findWithDefault 0 "jellybabies" m
data SummarizedJellyCount
= SummarizedJellyCount
{ basicCount :: JellyCount -- our basic information
, beansPerBaby :: Double -- extra information (ooo... expensive!)
}
summarize :: JellyCount -> SummarizedJellyCount
summarize j = SummarizedJellyCount j bpb
where
bpb = fromIntegral (beans j) / fromIntegral (babies j)
serializeSummary :: SummarizedJellyCount -> Text
serializeSummary (SummarizedJellyCount _ bpb)
= "Beans per Baby: " <> toFixed 2 bpb <> "\n"
serializeFullReport :: SummarizedJellyCount -> Text
serializeFullReport s@(SummarizedJellyCount (JellyCount beans babies) _)
= "JellyBeans: " <> showt beans <> "\n"
<> "JellyBabies: " <> showt babies <> "\n"
<> serializeSummary s
writeSummary :: FilePath -> SummarizedJellyCount -> IO ()
writeSummary fp s = writeFile fp (serializeSummary s)
writeFullReport :: FilePath -> SummarizedJellyCount -> IO ()
writeFullReport fp s = writeFile fp (serializeFullReport s)
main :: IO ()
main = do
let
-- We make a list of JellyCount, one for each bag of candy.
-- fmap is a standard function which applies our countJelly
-- function to each CandyBag in the list, producing a list as
-- output. This is (roughly) equivalent to part of the Python
-- for loop above.
counts :: [JellyCount] = fmap countJelly candyBags
-- We smash together all the counts to get the final count.
-- mconcat is another standard function that lets us smash
-- together a whole list that contains monoid elements.
finalCount :: JellyCount = mconcat counts
-- Do the summary
summarizedCount = summarize finalCount
-- The IO actions which create the reports:
writeSummary "summary.haskell.txt" summarizedCount
writeFullReport "full_report.haskell.txt" summarizedCount
#!/usr/bin/env python
#
# Python code from the blog post: "FP vs OO; A Better Way to Count Candy"
# by Jonathan Merritt.
#
# Run it as follows:
#
# $ python3 candy.py
#
# $ cat full_report.python.txt
# JellyBeans: 44
# JellyBabies: 18
# Beans per Baby: 2.44
#
# $ cat summary.python.txt
# Beans per Baby: 2.44
candy_bags = [
{ 'jellybeans': 42,
'jellybabies': 10,
'frogs': 8 },
{ 'jellybeans': 2,
'jellybabies': 8,
'nerds': 11 } ]
class JellyCounter:
def __init__(self, report_path: str):
self.beans_count = 0
self.babies_count = 0
self.beans_per_baby = 0.0
self.report_path = report_path
def add_bag(self, candy_bag):
self.beans_count += candy_bag['jellybeans']
self.babies_count += candy_bag['jellybabies']
def calculate_fraction(self):
self.beans_per_baby = self.beans_count / self.babies_count
def write_summary(self):
self.calculate_fraction()
with open(f'{self.report_path}/summary.python.txt', 'w') as out_file:
out_file.write(f'Beans per Baby: {self.beans_per_baby:.2f}\n')
def write_full_report(self):
with open(f'{self.report_path}/full_report.python.txt', 'w') as out_file:
out_file.write(f'JellyBeans: {self.beans_count}\n')
out_file.write(f'JellyBabies: {self.babies_count}\n')
out_file.write(f'Beans per Baby: {self.beans_per_baby:.2f}\n')
if __name__ == '__main__':
counter = JellyCounter(report_path='.')
for bag in candy_bags:
counter.add_bag(bag)
counter.write_summary()
counter.write_full_report()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment