【问题标题】:"Strategy Pattern" in HaskellHaskell 中的“策略模式”
【发布时间】:2014-02-10 12:25:30
【问题描述】:

在 OO 世界中,我有一个类(我们称之为“建议器”),它实现了一些接近“策略模式”的东西,以便在运行时提供算法的不同实现。作为学习 Haskell 的练习,我想重写它。

实际用例相当复杂,所以我将归结为一个更简单的例子。

假设我有一个类Suggester,它接受一个规则列表,并将每个规则作为过滤器应用于数据库结果列表。

每条规则都有“构建查询”、“查询后过滤器”和“计分器”三个阶段。我们基本上最终得到了一个满足以下要求的界面

buildQuery :: Query -> Query
postQueryFilter :: [Record] -> [Record]
scorer :: [Record] -> [(Record, Int)]

Suggestor 需要获取与此接口匹配的规则列表 - 在运行时动态地 - 然后按顺序执行它们。 buildQuery() 必须首先在所有规则中运行,然后是 postQueryFilter,然后是 scorer。 (即我不能将一个规则的函数组合成一个函数)。

在 scala 中我只是这样做

// No state, so a singleton `object` instead of a class is ok
object Rule1 extends Rule {
  def buildQuery ...
  def postQueryFilter ...
  def scorer ...
}

object Rule2 extends Rule { .... }

然后可以通过传递相关规则来初始化服务(在运行时根据用户输入定义)。

val suggester = new Suggester( List(Rule1, Rule2, Rule3) );

如果规则是单个函数,这将很简单 - 只需传递函数列表。然而,由于每个规则实际上是三个函数,我需要以某种方式将它们组合在一起,所以我有多个实现满足一个接口。

我的第一个想法是类型类,但是这些似乎不能完全满足我的需求——它们需要一个类型变量,并强制我的每个方法都必须使用它——但它们没有。

No parameters for class `Rule`

我的第二个想法是将每个模块放在一个 haskell 模块中,但由于模块不是“第一类”,我不能直接传递它们(而且它们当然不强制执行接口)。

第三次我尝试创建一个记录类型来封装函数

data Rule = Rule { buildQuery :: Query -> Query, .... etc }

然后为每个定义一个“规则”实例。在每个模块中完成此操作后,它可以很好地封装并且工作正常,但感觉就像是 hack,我不确定这是否适合在 haskell 中使用记录?

tl;dr - 如何将一组函数封装在一起,以便我可以将它们作为匹配接口的实例传递,但实际上不使用类型变量。

还是我完全是出于错误的心态?

【问题讨论】:

  • 这在什么方面感觉像是 hack?我认为这是一种完全可以接受的使用记录的方式。
  • 函数是数据。即,它们是一流的数据。它们可以被传递、返回和存储。当这看起来不再奇怪时,您将在通往 OO 启蒙的道路上顺利进行。在 OO 中,函数通常不是一等数据,因此我们将函数包装在对象中,就像在命令和策略模式中一样。
  • 同样,函数数据。没有把它们放在一起,因为无论如何它们已经是相同的东西了。如果你也想包含状态,你可能想使用lenses 之类的东西,但这与类型类无关。
  • @JamesDavies 不,您继续将它们放入记录中-有状态的计算是一元数据,但仍然是数据。这没有特殊的语法,因为它在 Haskell 中非常简单。您不需要类型类,它不是 hack,它只是包含一些功能等的记录。
  • 在 Haskell 中不需要策略模式,因为函数,甚至是有状态的函数都是一流的值,您可以在记录/ADT/作为值/作为参数或参数中传递它们。

标签: haskell interface strategy-pattern


【解决方案1】:

在我看来,您的解决方案不是“hack”,而是 OO 语言中的“策略模式”:它只需要解决语言的限制,特别是在缺少、不安全或不方便的 Lambdas/闭包/函数指针等,因此您需要一种“包装器”,以使其对该语言“可消化”。

“策略”基本上是一个函数(可能附带一些附加数据)。但是如果一个函数真的是语言的第一类成员——就像在 Haskell 中一样,就没有必要将它隐藏在对象壁橱中。

【讨论】:

  • 没错。通过反学习 OO 来学习 FP 时,以下翻译可能会有所帮助:复合模式 = 代数数据类型;访问者模式 = fold(r),迭代器模式 = 惰性。
  • 谢谢。最终结果令人惊讶地仍然与 OO 世界中的“策略”几乎相同。与数据结构中包含的接口匹配的一堆函数。无论是记录、结构还是类,设计最终都非常相似(只要它首先是无状态且纯粹的)。任何其他名称的玫瑰......当然,如果它包装单个函数,则不需要结构,因为它可以直接传递,但这在许多 OO 语言中也是一样的。我必须忘记的东西比我想象的要少得多(到目前为止)。
  • 这仅适用于单方法策略。
【解决方案2】:

只需像你一样生成一个 Rule 类型

data Rule = Rule
  { buildQuery :: Query -> Query
  , postQueryFilter :: [Record] -> [Record]
  , scorer :: [Record] -> [(Record, Int)]
  }

并构建一个通用的应用程序方法——我假设存在这样一个通用的东西,因为这些 Rules 旨在独立地对 SQL 结果进行操作

applyRule :: Rule -> Results -> Results

最后,您可以在任何地方实现任意数量的规则:只需导入 Rule 类型并创建适当的值。没有像在 OO 设置中那样为每个不同的规则赋予其自己的类型的先验理由。

easyRule :: Rule
easyRule = Rule id id (\recs -> zip recs [1..])

upsideDownRule :: Rule
upsideDownRule = Rule reverse reverse (\recs -> zip recs [-1, -2..])

如果你有Rules 的列表,你可以按顺序应用它们

applyRules :: [Rule] -> Results -> Results
applyRules []     res = res
applyRules (r:rs) res = applyRules rs (applyRule r res)

实际上只是伪装的foldr

applyRules rs res = foldr applyRule res rs

foo :: Results -> Results
foo = applyRules [Some.Module.easyRule, Some.Other.Module.upsideDownRule]

【讨论】: