我对Haskell相当新,但我试图学习一下。我决定写一个简单的自制计算器作为练习项目,我正在寻找一些更好的建模帮助。
我的想法是,由于酿造是一个线性过程,因此应该可以定义一堆"组件"代表酿造的各种状态。这是一个简化的酿造过程大纲(我已经将我试图建模的东西标记为斜体的类型或操作):
制作 mash 。这基本上是将谷物添加到水中。谷物是 Fermentable 的一种类型,也是迄今为止我的代码中唯一的一种。
Sparge 这种混合物意味着用水清洗谷物中的糖分,这样你就可以得到一种叫做麦芽汁的含糖液体。
煮沸麦芽汁和一些啤酒花,给出一个啤酒花麦芽汁。这可以重复几次,每次都添加更多的跃点。
将酵母和发酵添加到成品啤酒中。
到目前为止我所做的是一个简单的程序开始,我希望改进,我希望有一个指导之手。
首先,这个过程的顺序性使我立刻想到了monad!然而,到目前为止,我实现这一目标的尝试失败了似乎它应该能够以某种方式将操作链接在一起,如下所示:
initiateMash >>= addFermentable xxx >>= addFermentable yyy >>= sparge >>= addHops zzz >>= boil Minutes 60 >>= Drink!
我最初的想法是以某种方式使Monad的组件实例,但我无法弄清楚。然后我尝试制作某种类似于monad的brew步骤类型,有点像这样:
data BrewOperation a = Boiling a | Sparging a -- etc
instance Monad BrewOperation where ????
但这并没有结合在一起。有关我应该如何建模的任何建议?在我下面的类型中,我传递了上一步中的类型以保留历史记录,但我猜测有更好的方法。 Monad变形金刚?
我的另一个问题是关于代数类型以及何时使用记录语法以及何时不使用。我无法确定哪个是可取的,对此有什么好的指导方针吗?
另外,关于newtypes。在一个地方我想添加两个持续时间:但由于我没有添加运算符,我想知道最好的处理方法是什么。我应该把它作为" Num a"的一个实例。类?
这是我到目前为止编写的一些代码。 - 单位 newtype Weight = Grams Integer newtype Volume = Milliliters Integer newtype Bitterness = IBU整数 newtype持续时间=分钟整数
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
有关如何使这更好的建议?
编辑:为了澄清,我这样做是为了学习,所以我实际上并不是在寻找最漂亮的代码。我真的想知道如何以类似于我上面建议的方式进行排序。
答案 0 :(得分: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
答案 1 :(得分:3)
另一个答案是正确答案。你现在不需要monad,因为你没有“context”(状态,非确定性,效果......)的概念。但是,您可以使用Identity
monad单独表达自己,其中>>=
只是(包装)向后应用程序(m >>= k = k (runIdentity m)
):
import Control.Monad.Identity
result = runIdentity $ return (initiateMash v) >>= return . addFermentable yyy >>= return . sparge www
然后,您可以将return
隐藏在其他函数中,使其成为monadic - 即为其提供类似Monad m => ... -> m Mash
的类型。
恭喜你,你是astronaut!
严肃地说,有些库确实以这种方式暴露了monadic函数。例如,A*
搜索的astar包会公开高阶函数,aStar
和广义版aStarM
。后者不仅是monadic(它本身也是无用的),但是要求它的参数(它们本身用于计算距离,后继者和& c。)是monadic 。由于这个monad可以是任何东西,你可以使用Writer
monad并按照你访问的顺序记录图节点(通过让goal
谓词写下它的节点),或者住在{{1}并让你的邻居函数呼叫互联网寻找邻居,或通过变形金刚或其他任何东西组成几个monad。功能图库中有类似的功能。在你的情况下,它可能只是天文学(但是及时)。
最后,如果您的IO
是Duration
类型的新类型,则可以启用Num
并派生GeneralizedNewtypeDeriving
。