我对 Haskell 还很陌生,但我正在尝试学习一点。我决定写一个简单的自制计算器作为一个练习项目,我正在寻找一些帮助来更好地建模它。
我的想法是,由于酿造是一个线性过程,因此应该可以定义一堆代表酿造的各种状态的“组件”。这是酿造过程的简化大纲(我已将我尝试建模的事物标记为斜体类型或操作):
捣碎。_ 这基本上是在水中添加谷物。谷物是一种可发酵的,也是迄今为止我的代码中唯一的一种。
喷洒麦芽浆,意思是用水洗掉谷物中的糖分,这样你就得到了一种叫做麦芽汁的含糖液体。
将麦汁与一些啤酒花一起煮沸,制成酒花麦芽汁。这可以重复几次,每次添加更多的啤酒花。
在成品啤酒中加入酵母发酵。
到目前为止,我所拥有的只是一个我想改进的程序的简单开始,我希望得到指导。
首先,这个过程的顺序性让我立刻想到了单子!然而,到目前为止,我实现这一点的尝试都失败了。似乎它应该能够以某种方式将操作链接在一起,如下所示:
initiateMash >>= addFermentable xxx >>= addFermentable yyy >>= sparge >>= addHops zzz >>= boil Minutes 60 >>= Drink!
我最初的想法是以某种方式制作 Monad 的组件实例,但我无法弄清楚。然后我尝试制作某种 brew step 类型,它是 monad,有点像这样:
data BrewOperation a = Boiling a | Sparging a -- etc
instance Monad BrewOperation where ????
但这也没有结合在一起。关于我应该如何建模的任何建议?在下面的类型中,我传递上一步中的类型以保留历史记录,但我猜有更好的方法。单子变压器?
我的另一个问题是关于代数类型以及何时使用记录语法以及何时不使用。我真的无法决定哪个更可取,有什么好的指导方针吗?
另外,关于新类型。在一个地方我想添加两个 Duration:s 但由于我没有加法运算符,我想知道处理它的最佳方法是什么。我应该让它成为“Num a”类的一个实例吗?
这是我到目前为止编写的一些代码。-- 单位 newtype 重量 = 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
有什么建议可以让我做得更好吗?
编辑:为了澄清,我这样做是为了学习,所以我实际上并不是在寻找最漂亮的代码。我真的很想知道如何/是否可以以类似于我上面建议的方式进行排序。