6

动机

我目前正在从事一个小爱好项目,以尝试在 Haskell 中实现类似TaskJuggler的东西,主要是作为编写特定领域语言的实验。

我目前的目标是有一个小的 DSL 来构建 a 的描述Project,以及它的关联Tasks。还没有层次结构,尽管这将是我的下一个扩展。目前,我有以下数据类型:

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以获取任务列表,并为 选择默认值,例如 8 projectMaxHoursPerDay

但我以后希望能够做类似的事情:

project "LambdaBook" $ do
  maxHoursPerDay 4
  task "Web site"
  task "Marketing"

所以我maxHoursPerDay用来指定关于 a 的(未来)属性Project。我不能再Writer为此使用 a ,因为[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不是 a Monoid,但它当然可以变成 a Semigroup。名称/开始/结束是First,maxHoursPerDayLast,projectTasks[Task]. 我们不能在 a 上有一个Writermonad Semigroup,但我们可以有一个Writer可绑定的函子。

实际问题

使用第一个解决方案 - 专用“属性” Monoid- 我们可以使用单子的全部功能,但需要选择成本。我可以复制 and 中的可覆盖属性,Project后者ProjectProperties将每个属性包装在适当的幺半群中。或者我可以只编写一次幺半群并将其嵌入其中Project——尽管我放弃了类型安全(maxHoursPerDay 必须Just在我实际制定项目计划时!)。

可绑定函子消除了代码重复并保留了类型安全性,但代价是立即放弃语法糖,并且可能会带来长期使用痛苦的成本(由于缺少return/ pure)。

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

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

4

3 回答 3

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 -> ...

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

于 2013-02-09T23:18:55.267 回答
1

Google+ 上提出的一个有趣的解决方案是使用普通的Writermonad,但使用Endo Projectmonoid。与 一起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 可能是对语法的过度滥用)。

于 2013-02-11T23:19:51.667 回答
0

This is sort of a non-answer, but I feel it should be said.

Isn't record syntax good enough? Do you really need a DSL for marginally improved syntax?

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

Tangentially, a Racketeer once told me that Haskell only has one macro: do syntax. So Haskellers shoehorn everything into monads whenever they want to manipulate syntax.

于 2013-02-13T20:46:26.013 回答