为酿造操作一系列操作建模

eri*_*rik 2 modeling haskell sequencing

我对Haskell相当新,但我正在努力学习.我决定写一个简单的自制计算器作为练习项目,我正在寻找一些更好的建模帮助.

我的想法是,由于酿造是一个线性过程,应该可以定义一堆代表酿造的各种状态的"组件".这是一个简化的酿造过程大纲(我已经将我试图建模的东西标记为斜体的类型或操作):

  1. 制作一个土豆泥.这基本上是在水中加入谷物.谷物是Fermentable的一种类型,也是迄今为止我的代码中唯一的一种.

  2. 喷洒泥浆意味着用水冲洗谷物中的糖分,这样你就可以得到一种叫麦芽汁的含糖液体.

  3. 了一些共同的麦芽汁啤酒花,给人一种酒花的麦芽汁.这可以重复几次,每次添加更多的跃点.

  4. 将酵母和酵素加入成品啤酒中.

到目前为止我所拥有的是一个简单的项目开始,我希望改进,我希望有一个指导之手.

首先,这个过程的顺序性使我立刻想到了monad!然而,到目前为止,我实现这一目标的尝试失败了 看起来它应该能够以某种方式将操作链接在一起,如下所示:

initiateMash >>= addFermentable xxx >>= addFermentable yyy >>= sparge >>= addHops zzz >>= boil Minutes 60 >>= Drink!
Run Code Online (Sandbox Code Playgroud)

我最初的想法是以某种方式使Monad的组件实例,但我无法弄明白.然后我尝试制作某种类似于monad的brew步骤类型,有点像这样:

data BrewOperation a = Boiling a | Sparging a -- etc
instance Monad BrewOperation where ????
Run Code Online (Sandbox Code Playgroud)

但这也没有结合在一起.有关我应该如何建模的任何建议?在我下面的类型中,我传递了上一步的类型以保留历史,但我猜有更好的方法.Monad变形金刚?

我的另一个问题是关于代数类型以及何时使用记录语法以及何时不使用.我无法确定哪个是可取的,有什么好的指导方针吗?

另外,关于newtypes.在一个地方我想添加两个持续时间:但由于我没有添加运算符,我想知道最好的处理方法是什么.我应该把它作为"Num a"类的一个实例吗?

这是我到目前为止编写的一些代码. - 单位newtype Weight = Grams整数newtype Volume = Milliliters整数newtype Bitterness = IBU整数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
Run Code Online (Sandbox Code Playgroud)

有关如何使这更好的建议吗?

编辑:为了澄清,我这样做是为了学习所以我实际上并不是在寻找最漂亮的代码.我真的想知道如何以一种类似于我上面建议的方式进行排序.

Ped*_*ues 7

这是一系列纯粹的计算,这就是函数组合的用途:

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

一些函数需要重新排列其参数顺序.当你习惯了函数组合时,你开始以一种有利于组合的方式编写你的函数.

如果您更喜欢按相反顺序对计算进行排序,只需使用以下>>>运算符Control.Category:

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

Monads很适合很多东西,但在这种情况下,它们似乎是不必要的复杂功能,因为计算在纯粹的环境中非常适合.