私は Haskell にかなり慣れていませんが、少し学ぼうとしています。練習プロジェクトとして簡単な自家醸造計算機を作成することにしました。それをよりよくモデル化するための助けを探しています。
私の考えでは、醸造は直線的なプロセスであるため、醸造のさまざまな状態を表す一連の「コンポーネント」を定義できるはずです。醸造プロセスの簡単な概要を次に示します (型または操作としてモデル化しようとしたものをイタリック体でマークしました)。
マッシュを作ります。これは基本的に穀物を水に加えることです。Grains はFermentableの 1 つのタイプであり、これまでのコードで唯一のものです。
麦汁をスパージします。これは、麦汁と呼ばれる糖分の多い液体が得られるように、穀物の糖分を水で洗い流すことを意味します。
麦汁 をホップと いっしょ に煮てホップド ワートを 作る. これを数回繰り返すことができ、そのたびにホップを追加します。
完成したビールに酵母を加えて発酵させます。
これまでのところ、改善したいプログラムの簡単な始まりであり、導きの手を望んでいました.
まず第一に、プロセスのシーケンシャルな性質により、私はすぐにモナドを考えさせられます! ただし、これを実装する私の試みはこれまでのところ失敗しています。次のように、どうにかして操作を連鎖させることができるように思われます。
initiateMash >>= addFermentable xxx >>= addFermentable yyy >>= sparge >>= addHops zzz >>= boil Minutes 60 >>= Drink!
私の最初の考えは、どうにかして Monad のコンポーネント インスタンスを作成することでしたが、それがわかりませんでした。次に、モナドとなる何らかのブリューステップタイプを作成しようとしました。次のようなものです。
data BrewOperation a = Boiling a | Sparging a -- etc
instance Monad BrewOperation where ????
しかし、それも合わなかった。これをどのようにモデル化すべきかについての提案はありますか? 以下の型では、前のステップの型を渡して履歴を保持していますが、もっと良い方法があると思います。モナドトランスフォーマー?
私が持っている別の質問は、代数型と、いつレコード構文を使用し、いつ使用しないかについてです。どちらが好ましいかを本当に決めることはできません。これに関する適切なガイドラインはありますか?
また、ニュータイプについて。ある場所で 2 つの Duration:s を追加したかったのですが、加算演算子がないため、それを処理する最善の方法は何か疑問に思っていました。「Num a」クラスのインスタンスにする必要がありますか?
ここに私がこれまでに書いたいくつかのコードがあります。-- 単位 newtype 重量 = グラム 整数 newtype 容量 = ミリリットル 整数 newtype 苦味 = 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
これをより良くする方法の提案はありますか?
編集:明確にするために、私はこれを学ぶためにこれを行っているので、実際には最も美しいコードを探しているわけではありません. 上記で提案したのと同様の方法でこれをシーケンスする方法/可能性があるかどうかを本当に知りたいです。