例如,我有一个容器类型来保存具有共同特征的元素.我还提供了一些类型的元素.而且我也希望这个函数能够轻松扩展(其他人可以创建自己的元素类型并由我的容器保存).
所以我这样做:
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风格中执行此操作的正确方法是什么?
知道它究竟是什么元素
要知道这一点,您当然应该使用简单的ADT
data Element' = E1Element E1
| E2Element E2
| ...
Run Code Online (Sandbox Code Playgroud)
这样,您可以模式匹配容器中的哪一个.
现在,那与冲突
其他人可以制作自己的元素类型并由我的容器保留
它必须发生冲突!当允许其他人将新类型添加到元素列表中时,无法安全地匹配所有可能的情况.因此,如果你想匹配,唯一正确的是拥有一套封闭的可能性,就像ADT给你的那样.
OTOH,与你最初想到的存在主义一样,允许类型的类别是开放的.没关系,但只是因为确切的类型实际上不可访问,而只是由定义的公共接口forall e. ElementClass e.
存在在Haskell中确实有点皱眉,因为它们是如此OO-ish.但有时这是正确的做法,你的应用程序可能是一个很好的例子.
好的,我会尝试帮助一下.
第一:我假设你有这些数据类型:
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)
而不是具有很多的的容器E1和E2,而是使用一个容器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真的缺乏这个)我将更新我的答案.
首先,请确保阅读并理解"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)
这种方法的缺点是:
好处是:
你可以采取的第二种方法是将类型重新表述为"界面".我的意思是你的类型现在将被建模为记录类型,其每个字段构成一个"方法":
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)
这有一个好处,你现在可以拥有任意数量的子类,你可以轻松添加它们而无需修改现有的操作.它有以下两个缺点:
Element,并修改构造的每个现有函数Element.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的情况.那么,现在的问题是,你有很多的遍及正在测试的代码是否洒功能Element是E1或E2,这是以前写的E3曾经存在过.添加第三个子shell时,这些函数中有多少会中断?祝你好运,因为编译器无法帮助你!
请注意,我所描述的这种三种替代方案也存在于OOP中,有以下三种选择:
Dynamic解决方案的OOP对应方是使用向下转型.它具有相同的缺点,因为我解释上面有人能进来,添加一个新的子类,代码"偷窥",在运行时亚型可能不准备处理这个问题.因此,对于如何从OOP思维转变为Haskell思想这一更广泛的问题,我认为这种比较提供了一个很好的起点.OOP和Haskell提供了所有三种选择.OOP使#3变得非常容易,但这基本上可以让你自己挂绳; #2是许多OOP大师推荐你做的事情,如果你受到纪律处分就可以实现; 但OOP中的#1变得非常冗长.Haskell使#1最简单; #2并不难实现,但需要更仔细的预见("我是否为这类用户提供了正确的操作?"); #3是一个有点冗长且反对语言的人.