I'm fairly new to Haskell but I'm trying to learn a bit. I decided to write a simple homebrewing calculator as a practice project and I'm looking for some help modelling it better.
My idea is that since brewing is a linear process it should be possible to define a bunch of "components" representing the various states of the brew. Here is a simplified outline of the brewing process (I have marked the things I tried to model as types or operations in italic):
Make a mash. This is basically adding grains to water. Grains are one type of Fermentable and the only one I have in my code so far.
Sparge the mash which means to wash out the sugars in the grains with water so that you get a sugary liquid called the wort.
Boil the wort together with some hops, giving a hopped wort. This can be repeated a few times, adding more hops each time.
Add yeast and ferment into the finished beer.
What I have so far is a simple beginning of a program that I would like to improve and I was hoping for a guiding hand.
First of all, the sequential nature of the process makes me immediately think monads! However my attempts to implement this have so far failed. Seems like it should somehow be able to chain operations together, something like this:
initiateMash >>= addFermentable xxx >>= addFermentable yyy >>= sparge >>= addHops zzz >>= boil Minutes 60 >>= Drink!
My initial thought was to make the components instances of Monad somehow but I couldn't figure that out. Then I attempted to make some sort of brew step type that would be the monad, sort of like this:
data BrewOperation a = Boiling a | Sparging a -- etc
instance Monad BrewOperation where ????
but that didn't come together either. Any suggestions to how I should model this? In the types I have below I pass the type from the previous step along to keep the history but I'm guessing there is a better way. Monad transformers?
Another question I have is about the algebraic types and when to use record syntax and when not to. I can't really decide which is preferrable, are there any good guidelines for this?
Also, regarding the newtypes. In one place I wanted to add two Duration:s but since I don't have the addition operator I was wondering what the best way to handle that is. Should I make it an instance of the "Num a" class?
Here is some code I've written so far.
-- Units
newtype Weight = Grams Integer
newtype Volume = Milliliters Integer
newtype Bitterness = IBU Integer
newtype Duration = Minutes Integer
type Percentage = Integer
type Efficiency = Percentage
type Density = Float
type ABV = Percentage
-- Components
data Fermentable =
Grain { name :: String, fermentableContent :: Percentage } -- TODO: use content to calculate efficiency
data Hops = Hops { hopname :: String, alphacontent :: Percentage }
data Mash = Mash { fermentables :: [(Fermentable, Weight)], water :: Volume }
data Wort = Wort Mash Volume Density
data HoppedWort = HoppedWort { wort :: Wort, hops :: [(Hops, Duration)] }
data Beer = Beer HoppedWort Bitterness ABV
-- Operations
initiateMash :: Volume -> Mash
initiateMash vol = Mash { fermentables = [], water = vol }
addFermentable :: Fermentable -> Weight -> Mash -> Mash
addFermentable ferm wt mash =
Mash {
fermentables = (ferm, wt) : fermentables mash,
water = water mash
}
sparge :: Mash -> Volume -> Density -> Wort
sparge mash vol density = Wort mash vol density
addHops :: Wort -> Hops -> HoppedWort
addHops :: HoppedWort -> Hops -> HoppedWort
boil :: HoppedWort -> Duration -> HoppedWort
boil hoppedwort boilDuration =
let addDuration :: Duration -> (Hops, Duration) -> (Hops, Duration)
addDuration (Minutes boilTime) (h, Minutes d) = (h, Minutes $ d + boilTime)
in
hoppedwort { hops = map (addDuration boilDuration) $ hops hoppedwort} -- TODO, calculate boiloff and new density
ferment :: HoppedWort -> Density -> Beer
ferment hoppedwort finalgravity = Beer hoppedwort (IBU 0) 5 -- TODO: calculate IBU from (hops,dur) and ABV from gravity
Any suggestions of how I could make this nicer?
EDIT: For clarification, I'm doing this to learn so I'm actually not looking for the prettiest code. I would really like to know how/if this is possible to sequence in a manner similar to what I suggested above.