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

och*_*les 6 monads dsl haskell

动机

我目前正在开发一个小业余爱好项目,尝试在Haskell中实现类似TaskJuggler的功能,主要是作为编写域特定语言的实验.

我目前的目标是建立一个小型DSL来构建a的描述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)
Run Code Online (Sandbox Code Playgroud)

那里没什么太疯狂的,我相信你会同意的.

现在我想创建一个DSL来构建项目/任务.我可以使用Writer [Task]monad来构建任务,但这不会很好地扩展.我们现在可以做到以下几点:

project "LambdaBook" startDate endDate $ do
  task "Web site"
  task "Marketing"
Run Code Online (Sandbox Code Playgroud)

where project :: Text -> Date -> Date -> Writer [Task] a,运行Writer以获取任务列表,并选择默认值,例如8 for projectMaxHoursPerDay.

但我后来希望能够做到这样的事情:

project "LambdaBook" $ do
  maxHoursPerDay 4
  task "Web site"
  task "Marketing"
Run Code Online (Sandbox Code Playgroud)

所以我maxHoursPerDay用来指定关于a的(未来)属性Project.我不能再用Writer这个了,因为[Task]我无法捕捉到我需要的一切.

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

将"可选"属性分隔为它们自己的幺半群

我可以分成Project:

data Project = Project { projectName, projectStart, projectEnd, projectProperties }
data ProjectProperties = ProjectProperties { projectMaxHoursPerDay :: Maybe Int
                                           , projectTasks :: [Task]
                                           }
Run Code Online (Sandbox Code Playgroud)

现在我可以有一个实例Monoid ProjectProperties.当我跑步时,Writer ProjectProperties我可以做所有违约我需要建立一个Project.我认为没有理由Project需要嵌入ProjectProperties- 它甚至可以具有与上面相同的定义.

使用可绑定的仿函数 Semigroup m => Writer m

虽然Project不是Monoid,但它当然可以制成一个Semigroup.名称/开始/结束是First,maxHoursPerDayLastprojectTasks[Task].我们不能拥有一个Writermonad Semigroup,但我们可以拥有一个Writer可绑定的仿函数.

实际问题

通过第一个解决方案 - 专用的'属性' Monoid- 我们可以选择成本来使用monad的全部功能.我可以在Project和中复制overridable属性ProjectProperties,其中后者将每个属性包装在适当的monoid中.或者我可以只写一次monoid并将其嵌入其中Project- 虽然我放弃了类型安全(maxHoursPerDay 必须Just在我实际制作项目计划时!).

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

我在http://hpaste.org/82024(可绑定仿函数)和http://hpaste.org/82025(对于monad方法)中有两种方法的示例.这些例子有点超出了这篇SO帖子中的内容(已经很大了),而且Resource还有Task.希望这将表明为什么我需要在DSL中走得太远Bind(或Monad).

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

scl*_*clv 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 -> ...
Run Code Online (Sandbox Code Playgroud)

您现在需要函数组合,而不是编写器中的操作,但这种模式允许您从部分填充的记录开始,并设置那些需要设置一次且仅一次的内容,并具有足够的类型安全性。它甚至允许您对最终结果中各种设置值和未设置值之间的关系施加约束。