【问题标题】:mapping list of different types implementing same function?实现相同功能的不同类型的映射列表?
【发布时间】:2011-03-04 22:21:38
【问题描述】:

我想将函数应用于列表(映射)中的每个元素,但元素可能具有不同的类型,但都实现了相同的函数(此处为“putOut”),就像接口一样。但是我无法创建此“接口”类型的列表(此处为“可输出”)。

如何映射实现相同功能的不同类型的列表?

import Control.Monad

main :: IO ()
main = do
 mapM_ putOut lst
 where
  lst :: [Outputable] -- ERROR: Class "Outputable" used as a type
  lst = [(Out1 1),(Out2 1 2)]

class Outputable a where
 putOut :: a -> IO ()

-- user defined:

data Out1 = Out1 Int deriving (Show)
data Out2 = Out2 Int Int deriving (Show)

instance Outputable Out1 where
 putOut out1 = putStrLn $ show out1

instance Outputable Out2 where
 putOut out2 = putStrLn $ show out2

【问题讨论】:

    标签: haskell interface map types


    【解决方案1】:

    Haskell 不允许异构列表。因此,您无法列出 Outputables,因为您的 Out1Out2 是两种不同的类型,即使它们都属于同一个类型类。

    但有一种解决方法允许使用ExistentialQuantification 模拟异构列表。 请参阅 Haskell wikibook 中的 heterogeneous lists 示例。

    如何使用

    1. {-# LANGUAGE ExistentialQuantification #-}放在模块顶部

    2. 定义一个盒子类型,里面隐藏异类元素:

        data ShowBox = forall s. Show s => SB s
        heteroList :: [ShowBox]
        heteroList = [SB (), SB 5, SB True]
      
    3. 为盒子类型本身定义一个必要的类实例:

        instance Show ShowBox where
          show (SB s) = show s
      
    4. 使用框列表。

    一个例子

    你的例子可以改写为:

    {-# LANGUAGE ExistentialQuantification #-}
    
    main :: IO ()
    main = do
     mapM_ print lst
     putStrLn "end"
     where
      lst :: [Printable]
      lst = [P (Out1 1),P (Out2 1 2)]
    
    -- box type (2)
    data Printable = forall a . Show a => P a
    
    -- necessary Show instance for the box type (3)
    instance Show Printable where show (P x) = show x
    
    -- user defined:
    data Out1 = Out1 Int deriving (Show)
    data Out2 = Out2 Int Int deriving (Show)
    

    【讨论】:

    • 当用户无法在此列表中使用他们的用户定义类型时,我该如何设计我的程序模块化?
    • 他们是 :-) 这就是存在量化的目的。我认为您应该弄清楚数据类型和类型类之间的区别。例如,请参阅此问题的答案stackoverflow.com/questions/870919/…
    【解决方案2】:

    你确定你真的想把不同的类型放在一个列表中吗?

    你可以使用类似 jetxee 的例子来进行存在量化,但想想它的实际作用:你有一个未知类型的术语列表,而你唯一可以用它们做的事情应用 putOut 以获取 IO () 值。也就是说,如果“接口”只提供一个已知结果类型的函数,那么existentials列表和结果列表没有区别。前者唯一可能的用途是将其转换为后者,那么为什么要添加额外的中间步骤呢?改用这样的东西:

    main :: IO ()
    main = do
        sequence_ lst
        where lst :: [IO ()]
              lst = [out1 1, out2 1 2]
    
    out1 x = putStrLn $ unwords ["Out1", show x]
    out2 x y = putStrLn $ unwords ["Out2", show x, show y]
    

    起初这似乎违反直觉,因为它依赖于 Haskell 的一些不寻常的功能。考虑:

    • 没有进行额外的计算——惰性求值意味着showunwords等。除非执行 IO 操作,否则不会运行。
    • 简单地创建IO () 值不会产生任何副作用——它们可以存储在列表中,以纯代码的形式传递,等等。只有main 中的sequence_ 函数运行它们。

    同样的论点适用于“Show 的实例”列表等等。对于Eq 之类的实例,它效果不佳,您需要两个类型的值,但是存在列表不会更好,因为您不知道是否有两个值是同一类型。在 那种 的情况下,你所能做的就是检查每个元素是否等于它自己,然后你也可以(如上)创建一个 Bools 列表并完成它。


    在更一般的情况下,最好记住 Haskell 类型的类不是 OOP 接口。类型类是实现即席多态性的强大手段,但不太适合隐藏实现细节。 OOP 语言倾向于将 ad-hoc 多态性、代码重用、数据封装、行为子类型等混为一谈,将所有内容绑定到同一个类层次结构中;在 Haskell 中,您可以(并且通常必须)分别处理每一个。

    OOP 语言中的对象,粗略地说,是(隐藏的、封装的)数据的集合,捆绑了用于操作该数据的函数,每个函数都将封装的数据作为隐式参数(thisself , 等等。)。要在 Haskell 中复制这一点,您根本不需要类型类:

    • 将每个“类方法”写为常规函数,并明确self 参数。
    • 将每个函数部分应用于“封装”数据的值
    • 部分应用函数合并到一个单一的记录类型中

    记录类型替换接口;任何具有正确签名的函数集合都代表接口的实现。在某些方面,这实际上是更好的面向对象风格,因为私有数据被完全隐藏,只有外部行为被暴露。

    正如上面更简单的情况,这几乎完全等同于存在版本;函数的记录是通过将类型类的每个方法应用于每个存在而得到的。

    有些类型类不能很好地使用函数记录——例如Monad——它们通常也是相同的类型类,不能用传统的OOP接口表达,正如现代版本的 C# 所证明的那样,它广泛使用了 monadic 样式,但没有提供任何类型的通用 IMonad 接口。

    另请参阅 this article 涵盖我所说的相同内容。您可能还想查看 Graphics.DrawingCombinators 库的示例,该库提供可扩展、可组合的图形而不使用类型类

    【讨论】:

    • > "你确定你真的想把不同的类型放在一个列表中吗?"
    • @sisif:请参阅我的扩展答案,以更好地描述如何在 Haskell 中表示 OOP 风格的接口。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-10-13
    • 2019-11-28
    • 2012-03-06
    • 1970-01-01
    • 1970-01-01
    • 2019-03-31
    • 1970-01-01
    相关资源
    最近更新 更多