对于更安全类型的DSL,可绑定仿函数是一个有用的抽象吗?

时间:2013-02-09 13:49:03

标签: haskell monads dsl

动机

我目前正在开展一个小业余爱好项目,尝试在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]无法捕获我需要的所有内容。

我看到解决这个问题的两种可能性:

将“可选”属性分隔为它们自己的monoid

我可以将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。名称/开头/结尾为FirstmaxHoursPerDayLastprojectTasks[Task]。我们不能在Writer上拥有Semigroup monad,但我们可以拥有Writer可绑定的仿函数。

实际问题

使用第一个解决方案 - 专用'属性'Monoid - 我们可以选择成本来充分利用monad。我可以复制ProjectProjectProperties中的可覆盖属性,其中后者将每个属性包装在适当的monoid中。或者我可以只编写一次monoid并将其嵌入到Project中 - 虽然我放弃了类型安全(maxHoursPerDay 必须Just当我实际生成项目计划!)。

可绑定的仿函数会删除代码重复并保留类型安全性,但是会立即放弃语法糖,以及可能需要长期使用的成本(由于缺少return / pure)。

我在http://hpaste.org/82024(对于可绑定仿函数)和http://hpaste.org/82025(对于monad方法)有两种方法的示例。这些示例稍微超出了此SO帖子中的内容(已经足够大),并且ResourceTask一起使用。希望这能说明为什么我需要在DSL中Bind(或Monad)。

我很高兴甚至找到了可绑定仿函数的适用用途,所以我很高兴听到您可能有任何想法或经验。

3 个答案:

答案 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中。