动机
我目前正在开发一个小爱好项目,尝试在Haskell中实现类似TaskJuggler的功能,主要是作为编写域特定语言的实验 .
我目前的目标是 Build 一个小型DSL来构建 Project
的描述,以及与之关联的 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]
monad来构建任务,但这不会很好地扩展 . 我们现在可以做到以下几点:
project "LambdaBook" startDate endDate $ do
task "Web site"
task "Marketing"
project :: Text -> Date -> Date -> Writer [Task] a
,运行 Writer
以获取任务列表,并为 projectMaxHoursPerDay
选择默认值,例如8 .
但我后来希望能够做到这样的事情:
project "LambdaBook" $ do
maxHoursPerDay 4
task "Web site"
task "Marketing"
所以我使用 maxHoursPerDay
来指定关于 Project
的(未来)属性 . 我不能再使用 Writer
,因为 [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
不是 Monoid
,但它当然可以变成 Semigroup
. 名称/开始/结束是 First
, maxHoursPerDay
是 Last
, projectTasks
是 [Task]
. 我们不能在 Semigroup
上拥有 Writer
monad,但我们可以拥有一个 Writer
可绑定的仿函数 .
实际问题
通过第一个解决方案 - 专用'properties' Monoid
- 我们可以选择成本来充分利用monad的全部功能 . 我可以复制 Project
和 ProjectProperties
中的可覆盖属性,后者将每个属性包装在适当的monoid中 . 或者我可以写一次monoid并将其嵌入 Project
中 - 尽管我放弃了类型安全( maxHoursPerDay
必须是 Just
,当我实际制作项目计划时!) .
一个可绑定的仿函数移除了代码重复并保留了类型安全性,但是立即放弃了语法糖,以及可能需要长期使用的成本(由于缺少 return
/ pure
) .
我在http://hpaste.org/82024(对于可绑定仿函数)和http://hpaste.org/82025(对于monad方法)有两种方法的示例 . 这些例子有点超出了这篇SO帖子(已经足够大了),并且 Resource
和 Task
. 希望这将表明为什么我需要在DSL中尽可能地去 Bind
(或 Monad
) .
我很高兴甚至找到了可绑定仿函数的适用用途,所以我很高兴听到你可能有任何想法或经验 .
3 回答
您现在需要函数组合,而不是编写器中的操作,但是此模式允许您从部分填充的记录开始,并设置需要设置一次且仅具有足够类型安全性的一次 . 它甚至允许您对最终结果中各种set和unset值之间的关系施加约束 .
在Google上提出的一个有趣的解决方案是使用普通的
Writer
monad,但使用Endo Project
monoid . 与lens
一起,这产生了一个非常好的DSL:随着操作
哪个可以与原始DSL一起使用 . 这可能是我想要的最好的解决方案(尽管使用monad可能只是滥用语法) .
这是一种非答案,但我觉得应该说 .
记录语法不够好吗?你真的需要一个DSL来略微改进语法吗?
从切线来看,一个Racketeer曾经告诉我,Haskell只有一个宏:
do
语法 . 因此,无论何时他们想要操纵语法,Haskellers都会把所有东西都塞进monad中 .