6

動機

私は現在、Haskell でTaskJugglerのようなものを試して実装するためのちょっとした趣味のプロジェクトに取り組んでいます。これは主に、ドメイン固有言語を書く実験です。

Project私の現在の目標は、関連する とともに、の記述を構築するための小さな DSL を持つことTaskです。階層はまだありませんが、それが私の次の拡張になります。現在、次のデータ型があります。

data Project = Project { projectName :: Text
                       , projectStart :: Day
                       , projectEnd :: Day
                       , projectMaxHoursPerDay :: Int
                       , projectTasks :: [Task]
                       }
  deriving (Eq, Show)

data Task = Task { taskName :: Text }
  deriving (Eq, Show)

そこには何もおかしなことはありません。きっと同意していただけると思います。

ここで、プロジェクト/タスクを構築するための DSL を作成したいと考えています。モナドを使ってタスクを構築することはできますWriter [Task]が、これではうまくスケーリングできません。現在、次のことができる可能性があります。

project "LambdaBook" startDate endDate $ do
  task "Web site"
  task "Marketing"

Whereproject :: Text -> Date -> Date -> Writer [Task] aは、 を実行しWriterてタスクのリストを取得し、 に対して 8 などのデフォルト値を選択しますprojectMaxHoursPerDay

しかし、後で次のようなことができるようになりたいと思います。

project "LambdaBook" $ do
  maxHoursPerDay 4
  task "Web site"
  task "Marketing"

だから私はmaxHoursPerDayについての(将来の)プロパティを指定するために使用していますProject。必要なものすべてをキャプチャできないWriterため、これには a を使用できなくなりました。[Task]

この問題を解決するには、次の 2 つの可能性があります。

「オプションの」プロパティを独自のモノイドに分離

次のように分割できProjectます。

data Project = Project { projectName, projectStart, projectEnd, projectProperties }
data ProjectProperties = ProjectProperties { projectMaxHoursPerDay :: Maybe Int
                                           , projectTasks :: [Task]
                                           }

これで、インスタンスを作成できますMonoid ProjectProperties。実行するWriter ProjectPropertiesと、ビルドに必要なすべてのデフォルト設定を行うことができますProjectProject埋め込む必要がある理由はないと思いますProjectProperties-上記と同じ定義を持つことさえできます。

バインド可能なファンクターを使用するSemigroup m => Writer m

ProjectはありませんがMonoid、確かに にすることができますSemigroup。名前/開始/終了はFirstmaxHoursPerDayis Last、およびprojectTasksis[Task]です。Writerの上にモナドを持つことはできませんが、バインド可能なファンクターSemigroupを持つことはできます。Writer

実際の質問

最初の解決策 - 専用の「プロパティ」Monoid- を使用すると、選択したコストでモナドの全機能を使用できます。オーバーライド可能なプロパティをProjectProjectPropertiesで複製できます。後者は各プロパティを適切なモノイドでラップします。または、モノイドを 1 回記述して内部に埋め込むこともできますが、Project型の安全性はあきらめます (プロジェクト計画を実際に作成するときでmaxHoursPerDay なければなりません! )。Just

バインド可能なファンクターは、コードの重複を取り除き、型の安全性を保持しますが、構文糖衣を放棄するという当面のコストと、( return/がないためにpure) 作業が面倒になるという長期的なコストがかかる可能性があります。

http://hpaste.org/82024 (バ​​インド可能なファンクター用) とhttp://hpaste.org/82025 (モナド アプローチ用)に両方のアプローチの例があります。これらの例は、この SO 投稿 (既に十分な大きさでした) の内容を少し超えており、. 願わくば、これが私がDSL でどこまで(または)行く必要があるのか​​を示してくれることを願っています。ResourceTaskBindMonad

バインド可能なファンクターの適切な用途を見つけたことに興奮しているので、あなたの考えや経験を聞いてうれしいです.

4

3 に答える 3

4
data Project maxHours = Project {tasks :: [Task], maxHourLimit :: maxHours}

defProject = Project [] ()

setMaxHours :: Project () -> Project Double
setMaxHours = ...

addTask :: Project a -> Project a

type CompleteProject = Project Double...

runProject :: CompleteProject -> ...

storeProject :: CompleteProject -> ...

ライターでのアクションではなく、関数の合成が必要ですが、このパターンでは、部分的に入力されたレコードから始めて、設定が必要なものを1回だけ設定し、型の安全性を十分に確保できます。また、最終結果のさまざまな設定値と未設定値の間の関係に制約を課すこともできます。

于 2013-02-09T23:18:55.267 に答える
1

Google+ で提案された興味深い解決策は、通常のWriterモナドを使用することでしたが、Endo Projectモノイドを使用しました。とともにlens、これは非常に優れた DSL を生成します。

data Project = Project { _projectName :: String
                       , _projectStart :: Day
                       , _projectEnd :: Day
                       , _projectTasks :: [Task]
                       }
  deriving (Eq, Show)

makeLenses ''Project

施術に合わせて

task :: String -> ProjectBuilder Task
task name = t <$ mapProject (projectTasks <>~ [t])
  where t = Task name []

これは元の DSL で使用できます。これはおそらく、私が望むものに対する最良の解決策です (モナドを使用することは、とにかく構文の乱用が多すぎるのかもしれません)。

于 2013-02-11T23:19:51.667 に答える
0

これは一種の非回答ですが、言われるべきだと思います。

レコードの構文は十分ではありませんか? わずかに改善された構文のための DSL が本当に必要ですか?

defaultProject
  { projectName = "Lambdabook"
  , projectStart = startDate
  , projectEnd = endDate
  , tasks =
    [ Task "Web site"
    , Task "marketing"
    ]
  }

接線的に言えば、Racketeer は、Haskell には 1 つのマクロしかないと私に言ったことがあります:doシンタックスです。そのため、Haskeller は、構文を操作したいときはいつでも、すべてをモナドに押し込みます。

于 2013-02-13T20:46:26.013 に答える