2

我对 Haskell 还很陌生,但我正在尝试学习一点。我决定写一个简单的自制计算器作为一个练习项目,我正在寻找一些帮助来更好地建模它。

我的想法是,由于酿造是一个线性过程,因此应该可以定义一堆代表酿造的各种状态的“组件”。这是酿造过程的简化大纲(我已将我尝试建模的事物标记为斜体类型或操作):

  1. 捣碎。_ 这基本上是在水中添加谷物。谷物是一种可发酵的,也是迄今为止我的代码中唯一的一种。

  2. 喷洒麦芽浆,意思是用水洗掉谷物中的糖分,这样你就得到了一种叫做麦芽汁的含糖液体。

  3. 将麦汁与一些啤酒花一起煮沸,制成酒花麦芽汁。这可以重复几次,每次添加更多的啤酒花。

  4. 在成品啤酒中加入酵母发酵

到目前为止,我所拥有的只是一个我想改进的程序的简单开始,我希望得到指导。

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

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

有什么建议可以让我做得更好吗?

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

4

2 回答 2

7

这是一个纯计算链,这就是函数组合的用途:

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

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

如果您更喜欢按相反的顺序对计算进行排序,只需使用>>>操作符 from Control.Category

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

Monad 对很多事情都很有用,但在这种情况下,它们似乎是不必要的复杂性,因为计算非常适合纯设置。

于 2013-11-28T17:53:22.190 回答
3

另一个答案是正确的。您目前不需要 monad,因为您没有“上下文”的概念(状态、不确定性、效果......)。但是,您可以使用Identitymonad 以单子方式表达自己,其中>>=只是(包装)后向应用程序(m >>= k = k (runIdentity m)):

import Control.Monad.Identity

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

然后,您可以将returns 隐藏在您的其他函数中,使它们成为一元的 - 即,为它们提供类似Monad m => ... -> m Mash.

恭喜你,你是一名宇航员

严肃地说,一些库确实以这种方式公开了单子函数。例如,用于搜索的astarA*公开了一个高阶函数,aStar和一个通用版本 , aStarM。后者不仅是一元的(它本身是无用的),而且要求它的参数(它们本身用于计算距离、后继等的函数)是一元的。由于这个 monad 可以是任何东西,您可以使用Writermonad 并按照您访问的顺序记录图节点(通过让goal谓词写下它的节点),或者住在IO并让你的邻居函数调用互联网来寻找邻居,或者通过转换器组成几个单子,或者其他什么。功能图形库中有类似的功能。就您而言,这可能只是天文学(无论多么及时)。

最后,如果你的类型Duration是新Num类型,你可以启用GeneralizedNewtypeDeriving和派生Num

于 2013-11-29T03:08:54.757 回答