如何将我在OOP中的想法转换为Haskell?
例如,我有一个容器类型来保存具有公共字符的元素。我还提供了一些类型作为元素。我还希望这个函数可以很容易地扩展(其他人可以创建自己的元素类型并由我的容器保存) 因此,我:如何将我在OOP中的想法转换为Haskell?,haskell,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 d
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]
这很好,直到我需要单独处理元素。由于forall,函数“f::Element->IO()”无法知道它到底是什么元素
在Haskell风格中,正确的方法是什么?好的,我会尽力帮助你 第一:我假设您有以下数据类型:
data E1 = E1 String
data E2 = E2 Int
你对这两者都有一个合理的操作,我将调用say
:
say1 :: E1 -> String -> String
say1 (E1 s) msg = msg ++ s
say2 :: E2 -> String -> String
say2 (E2 i) msg = msg ++ show i
因此,没有任何类型类或东西,您可以做的是:
type Messanger = String -> String
不要使用批次为E1
和E2
的容器,而是使用批次为Messagner
s的容器:
sayHello :: [Messanger] -> String
sayHello = map ($ "Hello, ")
sayHello [say1 (E1 "World"), say2 (E2 42)]
> ["Hello, World","Hello, 42"]
我希望这能帮你一点忙-事情就是离开对象,转而看操作
因此,与其将对象/数据推送到一个应该处理对象数据和行为的函数,不如使用一个通用的“接口”来完成您的工作
如果你给我一些更好的类和方法的例子(例如,两种可能确实共享某些特性或行为的类型-String
和Int
在这方面确实缺乏),我将更新我的答案
知道它到底是什么元素
要知道这一点,您当然应该使用一个简单的ADT
data Element' = E1Element E1
| E2Element E2
| ...
这样,您就可以在容器中的哪一个上进行模式匹配
现在,这与
其他人可以创建自己的元素类型并由我的容器保存
而且一定会发生冲突!当其他人被允许向元素列表添加新类型时,就无法安全地匹配所有可能的情况。所以,如果你想匹配,唯一正确的方法就是拥有一组封闭的可能性,就像ADT给你的那样
OTOH,像您最初想到的一样,允许类型的类是开放的。没关系,但这只是因为实际上无法访问确切的类型,而只能访问由forall e定义的公共接口。元素e类
在哈斯凯尔,存在主义者确实有点不受欢迎,因为他们太过于保守了。但有时这是非常正确的做法,您的应用程序可能是一个很好的例子。首先,确保您阅读并理解。您的示例代码比需要的更复杂 基本上,您要问的是如何在Haskell中将值从超类型转换为子类型中执行等效的向下转换。这种操作本质上可能失败,因此类型类似于
元素->可能是E1
这里要问的第一个问题是:你真的需要吗?有两个互补的替代方案。首先:你可以用这样一种方式来表达你的“超类型”,即它只有有限的、固定数量的“子类型”。然后你实现你的类型就像一个并集:
data Element = E1 String | E2 Int
每次您想要使用元素模式匹配和预处理时,您都有特定于案例的数据:
processElement :: Element -> whatever
processElement (E1 str) = ...
processElement (E2 i) = ...
这种方法的缺点是:
联合类型只能有一组固定的子类别
每次添加子类别时,都必须修改所有现有操作,以便为其添加额外的匹配类别
正面是:
通过枚举类型中的所有子类,您可以使用编译器告诉您何时遗漏了一个子类
添加新操作很容易,并且不需要修改任何现有代码
第二种方法是将类型重新格式化为“接口”。我的意思是,您的类型现在将被建模为记录类型,其每个字段构成一个“方法”:
这样做的好处是,您现在可以拥有任意多的子类别,并且可以轻松地添加它们,而无需修改现有操作。它有以下两个缺点:
如果需要添加新操作,则必须向元素
类型添加“方法”(字段),并修改构成元素
的每个现有函数
元素
类型的使用者永远无法分辨他们正在处理的子类别,也无法获取特定于此子类别的信息。例如,消费者无法分辨特定的元素
是用makeE2
构建的,更不用说提取这样一个元素
封装的Int
(请注意,您的存在主义示例相当于这种“接口”方法,并且具有相同的优点和局限性。这只是不必要的冗长。)
但如果你真的坚持要有相当于沮丧的情绪,还有第三种选择:使用模块。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
这是最接近OOP向下投射操作的解决方案。这样做的缺点是,降级本质上不是类型安全的。让我们回到几个月后需要在代码中添加E3
子类的情况。现在的问题是,代码中有很多函数,它们测试元素是E1
还是E2
,它们是在E3
之前编写的。添加第三个子类时,这些函数中有多少会中断?祝你好运,因为编译器无法帮助你
请注意,我描述的这三种备选方案也存在于OOP中,包括以下三种备选方案:
union类型的OOP对应物是Visitor模式,这意味着可以轻松地向类型添加新操作,而无需修改其子类。(嗯,相对容易。访客模式是hella-ver
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