可绑定的functor是更安全的DSL类型的有用抽象吗?

动机

我目前正在开发一个小爱好项目,尝试在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 . 名称/开始/结束是 FirstmaxHoursPerDayLastprojectTasks[Task] . 我们不能在 Semigroup 上拥有 Writer monad,但我们可以拥有一个 Writer 可绑定的仿函数 .

实际问题

通过第一个解决方案 - 专用'properties' 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)

2 years ago

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值之间的关系施加约束 .

2 years ago

在Google上提出的一个有趣的解决方案是使用普通的 Writer monad,但使用 Endo Project monoid . 与 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 years ago

这是一种非答案,但我觉得应该说 .

记录语法不够好吗?你真的需要一个DSL来略微改进语法吗?

defaultProject
  { projectName = "Lambdabook"
  , projectStart = startDate
  , projectEnd = endDate
  , tasks =
    [ Task "Web site"
    , Task "marketing"
    ]
  }

从切线来看,一个Racketeer曾经告诉我,Haskell只有一个宏: do 语法 . 因此,无论何时他们想要操纵语法,Haskellers都会把所有东西都塞进monad中 .