如何将我在OOP中的想法转换为Haskell?

Mag*_*oud 3 haskell

例如,我有一个容器类型来保存具有共同特征的元素.我还提供了一些类型的元素.而且我也希望这个函数能够轻松扩展(其他人可以创建自己的元素类型并由我的容器保存).

所以我这样做:

class ElementClass
data E1 = E1 String
instance ElementClass E1
data E2 = E2 Int
instance ElementClass E2
data Element = forall e. (ElementClass e) => Element e
data Container = Container [Element]
Run Code Online (Sandbox Code Playgroud)

这很好,直到我需要单独处理元素.由于forall,函数"f :: Element - > IO()"无法知道它究竟是什么元素.

在Haskell风格中执行此操作的正确方法是什么?

lef*_*out 7

知道它究竟是什么元素

要知道这一点,您当然应该使用简单的ADT

data Element' = E1Element E1
              | E2Element E2
              | ...
Run Code Online (Sandbox Code Playgroud)

这样,您可以模式匹配容器中的哪一个.

现在,那与冲突

其他人可以制作自己的元素类型并由我的容器保留

它必须发生冲突!当允许其他人将新类型添加到元素列表中时,无法安全地匹配所有可能的情况.因此,如果你想匹配,唯一正确的是拥有一套封闭的可能性,就像ADT给你的那样.

OTOH,与你最初想到的存在主义一样,允许类型的类别是开放的.没关系,但只是因为确切的类型实际上不可访问,而只是由定义的公共接口forall e. ElementClass e.

存在在Haskell中确实有点皱眉,因为它们是如此OO-ish.但有时这是正确的做法,你的应用程序可能是一个很好的例子.


Car*_*ten 6

好的,我会尝试帮助一下.

第一:我假设你有这些数据类型:

data E1 = E1 String
data E2 = E2 Int
Run Code Online (Sandbox Code Playgroud)

而你对这两方面都有明智的操作我会打电话say:

say1 :: E1 -> String -> String
say1 (E1 s) msg = msg ++ s

say2 :: E2 -> String -> String
say2 (E2 i) msg = msg ++ show i
Run Code Online (Sandbox Code Playgroud)

那么你可以做什么没有任何类型类或东西是这样的:

type Messanger = String -> String
Run Code Online (Sandbox Code Playgroud)

而不是具有很多的的容器E1E2,而是使用一个容器MessagnerS:

sayHello :: [Messanger] -> String
sayHello = map ($ "Hello, ")

sayHello [say1 (E1 "World"), say2 (E2 42)]
> ["Hello, World","Hello, 42"]
Run Code Online (Sandbox Code Playgroud)

我希望这会对你有所帮助 - 事情就是离开对象并查看操作.

因此,不是将对象/数据推送到应该使用对象数据和行为的函数,而是使用通用的"接口"来完成您的工作.

如果你给我一些更好的方法的例子(例如两种类型可能确实共享一些特征或行为 - String并且Int真的缺乏这个)我将更新我的答案.


Lui*_*las 5

首先,请确保阅读并理解"Haskell Antipattern:Existential Typeclass".您的示例代码比它需要的更复杂.

基本上,你问的是如何在Haskell中执行等效的向下转换 - 从超类型转换为子类型的值.这种操作本质上可能会失败,因此类型就像Element -> Maybe E1.

这里要问的第一个问题是:你真的需要吗?对此有两种互补的替代方案.首先:你可以制定你的"超类型",使它只有一个有限的,固定数量的"子类型".然后你就像工会一样实现你的类型:

data Element = E1 String | E2 Int
Run Code Online (Sandbox Code Playgroud)

每次你想要使用Element模式匹配和presto时,你都有特定于案例的数据:

processElement :: Element -> whatever
processElement (E1 str) = ...
processElement (E2 i) = ...
Run Code Online (Sandbox Code Playgroud)

这种方法的缺点是:

  1. 您的联合类型只能有一组固定的子类.
  2. 每次添加子shell时,都必须修改所有现有操作,为其添加额外的匹配大小写.

好处是:

  1. 通过枚举类型中的所有子类,您可以使用编译器告诉您何时错过了一个子类.
  2. 添加新操作很简单,并且不需要您修改任何现有代码.

你可以采取的第二种方法是将类型重新表述为"界面".我的意思是你的类型现在将被建模为记录类型,其每个字段构成一个"方法":

data Element = Element { say :: String }

-- A "constructor" for your first subcase
makeE1 :: String -> Element
makeE1 str = Element str

-- A "constructor" for your second subcase
makeE2 :: Int -> Element
makeE2 i = Element (show i)
Run Code Online (Sandbox Code Playgroud)

这有一个好处,你现在可以拥有任意数量的子类,你可以轻松添加它们而无需修改现有的操作.它有以下两个缺点:

  1. 如果需要添加新操作,则必须在类型中添加"方法"(字段)Element,并修改构造的每个现有函数Element.
  2. Element类型的消费者永远不会知道他们正在处理哪个子案例,或者获取特定于此子案例的信息.例如,消费者不能告诉特定的Element开始构建makeE2,更不用说提取Int这样的Element封装.

(请注意,带有存在性的示例等同于此"接口"方法,并且具有相同的优点和限制.它只是不必要的冗长.)

但如果你真的坚持相当于一个向下倾斜,那么还有第三种选择:使用Data.Dynamic模块.甲Dynamic值是保存任何类型的实例化的一个单一的值的不可变容器Typeable类(GHC可以导出你).例:

data E1 = E1 String deriving Typeable
data E2 = E2 Int deriving Typeable

newtype Element = Element Dynamic

makeE1 :: String -> Element
makeE1 str = Element (toDyn (E1 str))

makeE2 :: Int -> Element
makeE2 i = Element (toDyn (E2 i))

-- Cast an Element to E1
toE1 :: Element -> Maybe E1
toE1 (Element dyn) = fromDynamic dyn

-- Cast an Element to E2
toE2 :: Element -> Maybe E2
toE2 (Element dyn) = fromDynamic dyn

-- Cast an Element to whichever type the context expects
fromElement :: Typeable a => Element -> Maybe a
fromElement (Element dyn) = fromDynamic dyn
Run Code Online (Sandbox Code Playgroud)

这是OOP向下转型操作的最接近的解决方案.这样做的缺点是,垂头丧气本质上不是类型安全的.让我们回到几个月后,您需要E3在代码中添加一个子shell的情况.那么,现在的问题是,你有很多的遍及正在测试的代码是否洒功能ElementE1E2,这是以前写的E3曾经存在过.添加第三个子shell时,这些函数中有多少会中断?祝你好运,因为编译器无法帮助你!

请注意,我所描述的这种三种替代方案也存在于OOP中,有以下三种选择:

  1. 联合类型的OOP对应是访问者模式,这意味着可以轻松地向类型添加新操作,而无需修改其子类.(嗯,相对容易.访客模式是hella verbose.)
  2. "接口"解决方案的OOP对应是100%编码到接口(或抽象类).这意味着您不仅要使用接口 - 它还意味着您的客户端代码永远不会 "在界面下"查看实际的实现类是什么; 它完全依赖于接口方法及其合同.
  3. Dynamic解决方案的OOP对应方是使用向下转型.它具有相同的缺点,因为我解释上面有人能进来,添加一个新的子类,代码"偷窥",在运行时亚型可能不准备处理这个问题.

因此,对于如何从OOP思维转变为Haskell思想这一更广泛的问题,我认为这种比较提供了一个很好的起点.OOP和Haskell提供了所有三种选择.OOP使#3变得非常容易,但这基本上可以让你自己挂绳; #2是许多OOP大师推荐你做的事情,如果你受到纪律处分就可以实现; 但OOP中的#1变得非常冗长.Haskell使#1最简单; #2并不难实现,但需要更仔细的预见("我是否为这类用户提供了正确的操作?"); #3是一个有点冗长且反对语言的人.