2

私は Haskell にかなり慣れていませんが、少し学ぼうとしています。練習プロジェクトとして簡単な自家醸造計算機を作成することにしました。それをよりよくモデル化するための助けを探しています。

私の考えでは、醸造は直線的なプロセスであるため、醸造のさまざまな状態を表す一連の「コンポーネント」を定義できるはずです。醸造プロセスの簡単な概要を次に示します (型または操作としてモデル化しようとしたものをイタリック体でマークしました)。

  1. マッシュを作ります。これは基本的に穀物を水に加えることです。Grains はFermentableの 1 つのタイプであり、これまでのコードで唯一のものです。

  2. 麦汁をスパージします。これは、麦汁と呼ばれる糖分の多い液体が得られるように、穀物の糖分を水で洗い流すことを意味します。

  3. 麦汁 をホップと いっしょ にホップド ワートを 作る. これを数回繰り返すことができ、そのたびにホップを追加します。

  4. 完成したビールに酵母を加えて発酵させます。

これまでのところ、改善したいプログラムの簡単な始まりであり、導きの手を望んでいました.

まず第一に、プロセスのシーケンシャルな性質により、私はすぐにモナドを考えさせられます! ただし、これを実装する私の試みはこれまでのところ失敗しています。次のように、どうにかして操作を連鎖させることができるように思われます。

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

これをより良くする方法の提案はありますか?

編集:明確にするために、私はこれを学ぶためにこれを行っているので、実際には最も美しいコードを探しているわけではありません. 上記で提案したのと同様の方法でこれをシーケンスする方法/可能性があるかどうかを本当に知りたいです。

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

モナドは多くのことに優れていますが、この場合、計算が純粋な設定にうまく適合するため、モナドは不必要な複雑さのように見えます。

于 2013-11-28T17:53:22.190 に答える