动机
我目前正在从事一个小爱好项目,以尝试在 Haskell 中实现类似TaskJuggler的东西,主要是作为编写特定领域语言的实验。
我目前的目标是有一个小的 DSL 来构建 a 的描述Project,以及它的关联Tasks。还没有层次结构,尽管这将是我的下一个扩展。目前,我有以下数据类型:
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]monad 来构建任务,但这不会很好地扩展。我们现在或许可以做到以下几点:
project "LambdaBook" startDate endDate $ do
task "Web site"
task "Marketing"
其中project :: Text -> Date -> Date -> Writer [Task] a,运行Writer以获取任务列表,并为 选择默认值,例如 8 projectMaxHoursPerDay。
但我以后希望能够做类似的事情:
project "LambdaBook" $ do
maxHoursPerDay 4
task "Web site"
task "Marketing"
所以我maxHoursPerDay用来指定关于 a 的(未来)属性Project。我不能再Writer为此使用 a ,因为[Task]无法捕获我需要的所有内容。
我看到解决这个问题的两种可能性:
将“可选”属性分离到它们自己的幺半群中
我可以分成Project:
data Project = Project { projectName, projectStart, projectEnd, projectProperties }
data ProjectProperties = ProjectProperties { projectMaxHoursPerDay :: Maybe Int
, projectTasks :: [Task]
}
现在我可以有一个实例Monoid ProjectProperties。当我运行时,Writer ProjectProperties我可以执行构建Project. 我想没有理由Project需要嵌入ProjectProperties- 它甚至可以具有与上述相同的定义。
使用可绑定函子Semigroup m => Writer m
虽然Project不是 a Monoid,但它当然可以变成 a Semigroup。名称/开始/结束是First,maxHoursPerDay是Last,projectTasks是[Task]. 我们不能在 a 上有一个Writermonad Semigroup,但我们可以有一个Writer可绑定的函子。
实际问题
使用第一个解决方案 - 专用“属性” Monoid- 我们可以使用单子的全部功能,但需要选择成本。我可以复制 and 中的可覆盖属性,Project后者ProjectProperties将每个属性包装在适当的幺半群中。或者我可以只编写一次幺半群并将其嵌入其中Project——尽管我放弃了类型安全(maxHoursPerDay 必须是Just在我实际制定项目计划时!)。
可绑定函子消除了代码重复并保留了类型安全性,但代价是立即放弃语法糖,并且可能会带来长期使用痛苦的成本(由于缺少return/ pure)。
我在http://hpaste.org/82024(用于可绑定函子)和http://hpaste.org/82025(用于 monad 方法)有这两种方法的示例。这些示例超出了这篇 SO 帖子中的内容(已经足够大了),并且Resource与Task. 希望这能说明为什么我需要在 DSL中走得那么远Bind(或)。Monad
我很高兴甚至找到了可绑定函子的适用用途,所以我很高兴听到您可能有的任何想法或经验。