【问题标题】:How to implement this OOP case in Haskell?如何在 Haskell 中实现这个 OOP 案例?
【发布时间】:2012-09-24 01:10:38
【问题描述】:

在项目中,我有几种不同的类型,定义在不同的模块中,每个都有相关的功能(功能名称相同,含义非常相似,所以以下有意义)。现在我想创建一个列表,其中可以(同时)拥有所有这些类型的实例。我能想到的唯一可能是这样的:

data Common = A{...} | B{...} | ...

但这意味着将定义保存在一个地方,而不是在不同的模块中(对于 A、B、...)。有没有更好的方法来做到这一点?

UPD

我对 haskell 比较陌生,并且编写了一些与我的学习相关的程序。在这种情况下,我有不同的FormalLanguage 定义方法:FiniteAutomataGrammars 等等。它们中的每一个都有共同的功能(isAcceptedrepresentation、...),所以有一个列表似乎合乎逻辑,其中元素可以是这些类型中的任何一种。

【问题讨论】:

  • 如果您的定义过于复杂以至于占用了整个文件,您应该考虑降低它们的复杂性,而不是在 Haskell 中镜像该架构。您能否举例说明您要建模的具体内容?
  • 我对haskell比较陌生,写了一些与我的学习相关的程序。在这种情况下,我有不同的FormalLanguage 定义方法:FiniteAutomataGrammars 等等。它们中的每一个都有共同的方法,因此有一个列表似乎合乎逻辑,其中元素可以是这些类型中的任何一种。

标签: oop haskell types


【解决方案1】:

通过假设正确的解决方案是将不同的类型存储在列表中,您将 OOP 思维带入 Haskell。我将从检验这个假设开始。

通常我们将不同类型存储在同构列表中,因为它们支持通用接口。为什么不直接将通用接口分解出来并将其存储在列表中?

很遗憾,您的问题没有描述通用接口是什么,所以我将仅介绍几个常见示例作为演示。

第一个示例是一组值,xyz,它们都支持 Show 函数,该函数具有以下签名:

(Show a) => a -> String

我们可以直接在值上调用 show 并将结果字符串存储在列表中,而不是存储我们稍后要显示的类型:

list = [show x, show y, show z] :: String

过早调用 show 不会受到任何惩罚,因为 Haskell 是一种惰性语言,在我们真正需要字符串之前不会实际评估 shows。

或者该类型可能支持多种方法,例如:

class Contrived m where
    f1 :: m -> String -> Int
    f2 :: m -> Double

我们可以将上述形式的类转换为等效的字典,其中包含将方法部分应用于我们的值的结果:

data ContrivedDict = ContrivedDict {
    f1' :: String -> Int,
    f2' :: Double }

...我们可以使用这个字典将任何值打包到我们期望它支持的通用接口中:

buildDict :: (Contrived m) => m -> ContrivedDict
buildDict m = ContrivedDict { f1' = f1 m, f2' = f2 m }

然后我们可以将这个通用接口本身存储在列表中:

list :: [buildDict x, buildDict y, buildDict z]

同样,我们没有存储不同类型的值,而是将它们的共同元素分解出来以存储在列表中。

但是,这个技巧并不总是有效的。病态的例子是任何期望两个相同类型的操作数的二元运算符,例如来自Num 类的(+) 运算符,它具有以下类型:

(Num a) => a -> a -> a

据我所知,没有很好的基于字典的解决方案来部分应用二元运算并将其存储以保证将其应用于相同类型的第二个操作数。在这种情况下,存在类型类可能是唯一有效的方法。但是,我建议您尽可能坚持使用基于字典的方法,因为它允许比基于类型类的方法更强大的技巧和转换。

有关此技术的更多信息,我建议您阅读 Luke Palmer 的文章:Haskell Antipattern: Existential Typeclass

【讨论】:

  • 据我所知,在实际的 OO 语言中也没有像二进制操作这样好的 OO 解决方案。根据我的经验,大多数要么有特殊的内置支持,要么依赖 C 风格的重载函数,要么使用神秘的元编程技巧。
  • 这个基于字典的解决方案在haskell中是惯用的吗?它完全符合我的需求,所以我会使用它。
  • 字典与类型类的一个好的经验法则是,如果你使用类型导向的推理,那么你应该使用类型类,否则你应该使用字典(因为类型类的目的是根据类型自动选择适当的字典)。但是,在此示例中,您没有使用任何类型导向的推理,而只是使用它来存储您已经预先选择的实例的方法。也就是说,你只是把它当成字典用,不如把它做成字典。
  • @chersanya 在我之前的评论中忘记@你了,但我还想补充一点,即使将字典存储在列表中通常也是代码异味的一个指标,而且你没有以惯用的方式做事哈斯克尔风格。我从来不需要在自己的代码中使用这个技巧。
  • @GabrielGonzalez,请看问题 UPD,您如何建议实施这样的场景?
【解决方案2】:

可能性很小:

可能性一:

data Common = A AT | B BT | C CT

AT、BT、CT分别在各自的模块中描述

可能性2:

{-# LANGUAGE ExistentialQuantification #-}

class CommonClass a where
    f1 :: a -> Int

data Common = forall a . CommonClass a => Common a

这与 OOP 超类几乎相同,但您不能进行“向下转型”。然后,您可以为所有模块中的公共类的成员声明实现。

@Gabriel Gonzalez 建议的可能性 3:

data Common = Common {
     f1 :: Int
}

因此,您的模块通过使用闭包对“私有”部分进行抽象来实现通用接口。

但是,Haskell 设计通常与 OOP 设计完全不同。虽然可以在 Haskell 中实现每个 OOP 技巧,但它可能是非惯用的,因此 @dflemstr 表示欢迎提供有关您的问题的更多信息。

【讨论】:

  • 从技术上讲,可能性 3 只是 Int,而不是 a -> Int。不需要额外的数据类型。
  • 是的,我已经发现了我的错误并修复了。我认为这种方法类似于 javascript 模块模式。
  • 顺便说一句,“函数记录”方法本质上是用 Haskell 编码的 OOP,由于缺少子类型和使用 OO 风格的语法糖而略微步履蹒跚。它也往往是 Haskell 中最好的(和惯用的)设计,正是针对 OOP 被发明来解决的各种问题。
猜你喜欢
  • 1970-01-01
  • 2021-11-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多