我目前正在开展一个小业余爱好项目,尝试在Haskell中实现类似TaskJuggler的功能,主要是作为编写域特定语言的实验。
我目前的目标是建立一个小型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]
。我们不能在Writer
上拥有Semigroup
monad,但我们可以拥有Writer
可绑定的仿函数。
使用第一个解决方案 - 专用'属性'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
)。
我很高兴甚至找到了可绑定仿函数的适用用途,所以我很高兴听到您可能有任何想法或经验。
答案 0 :(得分: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 -> ...
您现在需要功能组合,而不是编写器中的操作,但是此模式允许您从部分填充的记录开始,并设置需要设置一次且仅具有足够类型安全性的一次。它甚至允许您对最终结果中各种set和unset值之间的关系施加约束。
答案 1 :(得分:1)
在Google+上提出的一个有趣的解决方案是使用普通Writer
monad,但使用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一起使用。这可能是我想要的最佳解决方案(尽管使用monad可能只是滥用语法)。
答案 2 :(得分:0)
这是一种不答案,但我觉得应该说。
记录语法不够好吗?你真的需要一个DSL来改善语法吗?
defaultProject
{ projectName = "Lambdabook"
, projectStart = startDate
, projectEnd = endDate
, tasks =
[ Task "Web site"
, Task "marketing"
]
}
Tangeically,一个Racketeer曾告诉我,Haskell只有一个宏:do
语法。因此,只要他们想要操纵语法,Haskellers就会把所有东西都塞进monad中。