Domanda

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):

  1. 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.

  2. 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.

  3. Boil the wort together with some hops, giving a hopped wort. This can be repeated a few times, adding more hops each time.

  4. 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.

È stato utile?

Soluzione

This is a chain of pure computations, and that's what functions composition is for:

drink . ferment vvv . boil (Minutes 60) . addHops zzz . sparge www . addFermentable yyy . addFermentable xxx . initiateMash

Some of functions would need to have their arguments order re-arranged. When you get used to function composition, you start to write your functions in a way that favors composition.

If you prefer to sequence your computations by the reverse order, just use the >>> operator from Control.Category:

initiateMash >>> addFermentable xxx >>> addFermentable yyy >>> sparge www >>> addHops zzz >>> boil (Minutes 60) >>> ferment vvv >>> drink

Monads are great for a lot of things but in this case they just seem to be needless complication, since the computations fit nicely in a pure setting.

Altri suggerimenti

The other answer is the correct one. You currently don't need a monad since you have no notion of "context" (state, nondeterminism, effects, ...). You can, however, express yourself monadically using the Identity monad, in which >>= is just (wrapped) backwards application (m >>= k = k (runIdentity m)):

import Control.Monad.Identity

result = runIdentity $ return (initiateMash v) >>= return . addFermentable yyy >>= return . sparge www

You can then hide the returns inside your other functions, making them monadic -- i.e., giving them types like Monad m => ... -> m Mash.

Congratulations, you're an astronaut!

In all seriousness, some libraries do expose monadic functions in this way. For instance, the astar package for A* search exposes a higher-order function, aStar, and a generalized version, aStarM. The latter is not only monadic (which would, by itself, be useless), but requires its arguments (themselves functions for computing distance, successors, &c.) to be monadic. Since this monad can be anything, you could use a Writer monad and log the graph nodes in the order you visit (by having the goal predicate write down its node), or live in IO and have your neighbour function call out to the internet to find neighbours, or compose several monads via transformers, or whatever. There's similar functionality in the Functional Graph Library. In your case, it's probably just astronomy (however timely).

Finally, if your Duration is a newtype over a Num type, you can just enable GeneralizedNewtypeDeriving and derive Num.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top