【问题标题】:An heterogeneous indexed structure without Existential Types?没有存在类型的异构索引结构?
【发布时间】:2012-10-27 06:35:54
【问题描述】:

我正在尝试构建异构索引结构并提出以下解决方案,但 I was told not to use existential types.

你能找到更好的解决方案吗?

我想保持接口定义(typeclass)和具体实现(datainstance)之间的分离。 编辑遵循@hammar的评论:在实际应用中,值不是Shown,而是简单地存储一个查询;还 myData 是更复杂的附加记录。

如果这可以带来更好的解决方案,那么确切的要求是构建地图地图(内部地图)。每个内部映射都是同质的,形式为Map String a,但是每个内部映射可能对其值强制使用不同的类型。您也可以将其视为两级索引结构。实现不必使用Data.Map,但必须高效。

{-# LANGUAGE ExistentialQuantification #-}
module Scratch.SO_ExtistentialTypes where

import Data.Map

data HeteroValue = forall a. Show a => HV a 

instance Show HeteroValue where
    show (HV b) = show b

type MyMap = Map String HeteroValue

class MyClass c where 
    getMyMap :: c -> MyMap

data MyData = MyData {
    myMap ::  MyMap
}

instance MyClass MyData where
    getMyMap = myMap

这个 sn-p 可以使用 ghci 运行

let myMap = fromList [("key1", HV "abc"), ("key2", HV 123)] :: MyMap
let myData = MyData myMap
getMyMap myData 

【问题讨论】:

  • @hammar Show 只是为了在 ghci 中测试这个 sn-p。在实际应用中,这些值是不显示的,只是存储和查询。
  • 真正的问题不是您是否应该使用存在类型或其他此类功能,而是您是否应该尝试存储这样的异构值。为什么你需要它们是异质的?你真正想在这个索引结构中存储什么?静态类型是你的朋友。静态类型将您从痛苦的世界中拯救出来。动态类型不是一个很酷的语言功能,它是一个 hack。你可以放任何你喜欢的东西,但另一方面,在运行时世界可以给你任何它喜欢的东西。
  • @AndrewC 这是一种意见。我的理解是当你写'forall a.合同 a =>` 你对“世界”的所有要求是他们“给你”的东西尊重“合同”。这完美地反映了我的意图。也许我的世界观更“动态”? :)
  • 是的,意见。如果你真的是这个意思,你可以这样 - 看我的回答。除了您想以 OO 样式对其进行编码之外,您还没有阐明程序的目的。你想存储什么?存储后你想用它做什么?

标签: haskell existential-type


【解决方案1】:

这是面向对象语言的一个很好的模式,但它是众所周知的Haskell antipattern。 阅读那篇文章。我希望你阅读它胜过我所说的任何其他内容。

另见this answer,但附带条件是我认为 GADT 比存在类型更优雅(见下文)。


请尝试找到编写程序的最佳函数式编程方式,而不是重新实现面向对象编程的最佳函数式编程方式。除了希望以 OO 风格进行编程之外,您还没有明确代码的任何目的。

(想象一下 Craig,一个刚接触 Java 的 C 程序员,他试图找到如何创建指向结构数组的指针,或者在尝试创建具有 malloc 功能的方法时卡住,或者因为没有指针而感到沮丧算术。Java 程序员珍妮特会问克雷格为什么他想自己做指针,垃圾收集有什么问题,以及当他们有内置边界检查的数组时,为什么会有人想要指针算术?真的是克雷格最好先学习编程惯用的 Java,然后再决定他真的不能没有 C 的哪些特性。OO 与 C 的“尽可能接近机器但仍然相对”的范式不同-机器独立的哲学。Craig 应该把学习新范式作为第一要务。这可能会让他成为一个更好的 C 程序员。不要去法国只说英语,看 CNN 和吃麦当劳!有一种感觉你应该是试图写你的 c尽可能以实用的方式颂歌。)


如果您真的希望获得关于您的数据的其他信息,而不是它遵守您的合同,那么一种方法是使用 GADT。您应该知道 Haskell 会要求您坚持索赔;没有演员可以让你摆脱一个轻率的设计决定。 (强制转换是一种将编译时检查转变为运行时检查的方法,或者换一种说法,一种将编译时错误转变为运行时错误的方法。我不认为这是一件好事。)

{-# LANGUAGE GADTs #-}

class Contract a where
   toString :: a -> String
   fromInts :: Int -> Int -> a
   alter :: a -> a
   -- other functionality

data Encapsulated where
   Encapsulate :: Contract a => a -> Encapsulated

现在,一旦您封装了您的数据,您就可以对它做任何您想做的事情,就好像它是一个普通的数据类型一样,并且可以像这样恢复任何 Contracted 功能:

munged :: Encapsulated -> String
munged (Encapsulate a) = toString.alter.alter.alter $ a

如果你愿意,你可以在一张地图中存储一大堆 Encapsulated 数据,无需为你的存在做任何特别的事情或重新实现 Data.Map,因为,这里是强大的函数范式:@987654328 @ 对您的数据做出任何假设。它是 parametric 多态性,适用于任何事物。任何东西,甚至功能。它所做的唯一假设是您的密钥是可排序的 (Ord k =>) 并且您的数据是同质的(我们的 GADT 是同质的,尽管是由异构数据制成的)。


这是满足您要求的一种方式,但如果我们知道您想要它的目的,我们可以为您提供更好的建议。 (可能是另一个新问题!)请认真阅读我链接到的文章,看看您是否可以将您的类实现为充满函数/结果的数据类型,并将您的实例实现为该数据类型的函数。

【讨论】:

  • 感谢您的意见。我读过这篇文章,它基本上提倡使用data+functions(一个非常Haskellish 的习惯用法)而不是使用classes+instances(更接近OO 编程习惯用法)。我要知道 Haskell 提供任何有根据的意见。我所知道的是,尽管有这篇文章,我在我阅读的代码中看到了很多forallclassesinstances,包括库。关于僵化的类型系统,我相信这更像是 Haskell 的一个特性而不是函数式编程(你可以用任何语言做 FP,有些更适合)。
  • 最后,很多很多年的商业应用程序编程(在 OO 中,是的,但也在 Cobol 中:))告诉我,在大型应用程序中,通用性很重要部分彼此更独立,更可重用。这里给出的所有解决方案似乎都需要放弃一些通用性(GADT 对我来说仍然很神秘)。美好的。点好。但我想知道我的选择是什么,以及在放弃一些选择之前我会采取什么措施。我似乎没有很多大型 Haskell 应用程序,更不用说商业应用程序了,我想知道我面临的困难是否是解释的一部分。
  • 无论如何,再次感谢您的意见,它拓宽了我的视野(对错别字感到抱歉)
  • @BGR 最容易放弃的事情是将你的类型混合在一起。那会让头痛消失。如果您不想放弃 OO,也许 Object Haskell 适合您,如果您不想离开 JVM,也许 Scala 会适合您。然而,一旦你习惯了 Haskell 的静态类型,它就很好,并且确实给你带来了显着的性能提升,同时让你以新的方式思考。我仍然很想知道您正在开发什么应用程序。
  • 不要误会我的意思:我不介意放弃 OO,因此我对 SO 提出了问题。我试试上面的建议。我知道 Scala,但我正试图摆脱 JVM,而不是回到 C 或“脚本”。这项工作与 DHT 实现有关。我(最近)的职能背景是 EDI 和中间件系统。
【解决方案2】:

进行“异构集合”的一种方法是使用Data.Dynamic

module Scratch.SO_Dyn where

import Data.Dynamic
import Data.Map

type MyMap = Map String Dynamic

class MyClass c where 
    getMyMap :: c -> MyMap

data MyData = MyData {
    myMap ::  MyMap
}

instance MyClass MyData where
    getMyMap = myMap

您希望放入此地图的数据必须派生 Typeable。
使用{-# LANGUAGE DeriveDataTypeable #-}deriving (Data, Typeable), 另见http://www.haskell.org/ghc/docs/7.6.1/html/users_guide/deriving.html#deriving-typeable

然后您可以使用toDyn 将数据转换为Dynamic 类型,并使用fromDynamic 安全地将其从Dynamic 类型转换。


虽然这是一种完全有效的方法,但我和许多其他 Haskeller 强烈建议您考虑制作自定义数据类型,而不是诉诸真正的异构集合。假设(本着万圣节的精神)您知道一个事实,您将在此地图中放入的唯一类型的东西是 Cats、Witches 和 Ghouls。

data Cat = ...
data Witch = ...
data Ghoul = ...

通过简单地标记每个可能的选项,您可以稍后确定每个选项是什么。

data HeteroValue
  = DarkOmen Cat
  | Hag Witch
  | Haunting Ghoul

case (Map.lookup "Midnight visitor" theMap) of
  Just (DarkOmen cat) -> hiss cat
  Just (Hag witch) -> cackle witch
  Just (Haunting ghoul) -> spook ghoul
  Nothing -> error ...

【讨论】:

  • 谢谢。我将仔细研究它并尝试使用自定义数据类型,如果不使用 Data.Dynamic。为什么我要模拟是 Java、C# 等中非常常见的模式,即定义一个 ìnterface
  • .../mixin 然后创建一个实现 interface/mixin 的对象注册表。我发现在 Haskell 中定义这些对象/值最接近的是forall a. Interface a =>。定义自定义数据会降低存储库的通用性,但在我的(近距离)情况下我可能会侥幸逃脱,否则我将求助于使用Data.Dynamic
  • 在 OO 中这是一个很好的模式,但在 haskell 中却是 antipattern。如果您正在使用函数式编程,请找到一种编写代码的函数式方式。
  • 每种方法都有优缺点;虽然有些人(如 Luke Palmer)认为最好避免使用类型类,除非在某些情况下,但其他人(如 Edward Kmett)则将类型类用于 OO 式编程。 (这些观点不一定是矛盾的,但是关于什么是“惯用的”和什么是“反模式”,肯定有很多灰色地带。)如果你想在 Haskell 中做类似 OO 的事情,那么你应该查看“lenses” "。
  • @DanBurton 确实如此。它可以做得很好。我认为WX haskell 是 Haskell 中一个不错的面向对象库(它很好地使用存在论作为属性),但我也认为在类型类上弹出存在量化并将它们全部放在数据结构中,因为这就是你在 OO 中所做的语言不是惯用的。我也解释卢克帕尔默反对存在类而不是一般的类型类。他确实建议仔细考虑您是否需要一个类型类,但这通常是个好建议。
【解决方案3】:

显然,您对HeteroValue 唯一能做的就是将其转换为show,即将其转换为String。考虑到这一点,存储值没有意义,您也可以只存储转换后的字符串:

type MyMap = Map String String

data HeteroData = HD { getShow :: String }
type MyMap = Map String HeteroData

这很容易被其他类型类采用。

如果您改为在 HeteroValue 上执行模式匹配等操作,则存在类型是一个很好的解决方案。

【讨论】:

  • 你是对的,但 Show 类只是作为示例。在实际应用中,Show 被一个自定义类取代,该类强制执行映射中的所有值都必须实现的行为,即使它们具有异构类型。
猜你喜欢
  • 1970-01-01
  • 2014-01-02
  • 1970-01-01
  • 2022-01-18
  • 1970-01-01
  • 1970-01-01
  • 2012-07-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多