动机
我目前正在从事一个小爱好项目,以尝试在 Haskell 中实现类似TaskJuggler的东西,主要是作为编写特定领域语言的实验。
我目前的目标是有一个小的 DSL 来构建 a 的描述Project
,以及它的关联Task
s。还没有层次结构,尽管这将是我的下一个扩展。目前,我有以下数据类型:
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 上有一个Writer
monad 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
我很高兴甚至找到了可绑定函子的适用用途,所以我很高兴听到您可能有的任何想法或经验。