【问题标题】:List of showables OOP-style in Haskell?Haskell中的可展示对象列表OOP风格?
【发布时间】:2015-11-07 19:13:06
【问题描述】:

我想建立一个具有一个共同属性的不同事物的列表,即它们可以变成字符串。面向对象的方法很简单:定义接口Showable 并让感兴趣的类实现它。当您无法更改类时,第二点原则上可能是一个问题,但让我们假设情况并非如此。然后你创建一个Showables 的列表,并用这些类的对象填充它,没有任何额外的噪音(例如,向上转换通常是隐式完成的)。 Java 中的概念证明is given here

我的问题是我在 Haskell 中有哪些选项?下面我将讨论我尝试过但并不真正让我满意的方法。

方法 1:存在主义。有效但丑陋。

{-# LANGUAGE ExistentialQuantification #-}
data Showable = forall a. Show a => Sh a

aList :: [Showable]
aList = [Sh (1 :: Int), Sh "abc"]

对我来说,这里的主要缺点是在填写列表时需要Sh。这非常类似于在 OO 语言中隐式完成的向上转换操作。

更一般地说,已经在语言中的东西的虚拟包装器Showable - Show 类型类 - 在我的代码中增加了额外的噪音。不好。

方法 2:谓语。想要但不起作用。

对我来说,最直接的类型和我真正想要的是:

{-# LANGUAGE ImpredicativeTypes #-}
aList :: [forall a. Show a => a]
aList = [(1 :: Int), "abc"]

除此之外 (as I heard)ImpredicativeTypes 是“最好的情况是脆弱的,最坏的情况是破碎的” 它不编译:

Couldn't match expected type ‘a’ with actual type ‘Int’
  ‘a’ is a rigid type variable bound by
      a type expected by the context: Show a => a

"abc" 也出现同样的错误。 (注意类型签名 1:没有它我会收到更奇怪的消息:Could not deduce (Num a) arising from the literal ‘1’)。

方法 3:Rank-N 类型以及某种功能列表(差异列表?)。

比起有问题的ImpredicativeTypes,人们可能更喜欢更稳定和被广泛接受的RankNTypes。这基本上意味着:移动 需要将类型构造函数(即[])之外的forall a. Show a => a 转换为普通函数类型。因此,我们需要将列表表示为普通函数。我几乎没有听说有这样的陈述。我听说的一个是差异列表。但是在Dlist package 中,主要类型是好旧的data,所以我们回到表语。我没有进一步研究这一行,因为我怀疑它可能会产生比方法 1 更冗长的代码。但如果你认为不会,请给我一个例子。

底线:你将如何在 Haskell 中完成这样的任务?您能否提供比 OO 语言更简洁的解决方案(尤其是代替填写列表 - 请参阅方法 1 中的代码注释)?您能否评论一下上面列出的方法的相关性?

UPD(基于第一个 cmets):为了便于阅读,这个问题当然被简化了。真正的问题更多是关于如何存储共享相同类型类的东西,即以后可以通过多种方式处理(Show 只有一种方法,但其他类可以有不止一种)。这会排除建议在填写列表时应用show 方法的解决方案。

【问题讨论】:

  • Java 和 Haskell 之间有一个您缺少的关键区别。在 Java 中,类型信息在向上转换时不会丢失,因此您可以再次向下转换并在以后执行其他有用的非Stringy 事情(条件是选择正确的向下转换类型)。在 Haskell 中,一旦你向上转换,类型信息就会丢失并且你不能再次向下转换。因此,您不妨只拥有Strings 的列表。另请参阅Dynamic,它提供了比“just String”更丰富的界面。
  • 我不太喜欢这个问题的标题。它声称语言/范式之间存在冲突,可能会呼吁一方的人们参加语言战争。
  • 这是一个已知的反模式,也在Luke Palmer's blog 中讨论过。虽然我并不完全同意整篇博文,但我相信他提出的基本观点是扎实的。
  • 从热门网络问题优化的标题中,我准备好以基于意见的方式投票结束。我对问题的质量感到惊喜,但是一旦你 15 分钟的 HNQ 结束,尝试将标题更改为不太可能吸引错误答案的名称。
  • 对于它的价值,您的第二次尝试(使用禁言类型)与第一次不同,绝对不是您想要的。 [forall a. Show a => a]forall a. Show a => a 的居民列表。这种类型的居民只有一个——底部。没有其他可能的值可以成为调用者要求的 any Show。没有 Int 可以容纳那种类型,没有 String ......什么都没有。从字面上看,这与您最初想要的相反。你可能想要[exists a. Show a => a],或者,skolemized,[(forall a. Show a => a -> r) -> r]

标签: haskell


【解决方案1】:

由于 Haskell 中的求值是惰性的,那么创建一个实际字符串的列表怎么样?

showables = [ show 1, show "blah", show 3.14 ]

【讨论】:

  • 无论如何,您都需要将它们收集到列表中,然后您可以map show
  • @arrowd 这不是很好的类型:在应用show 之前,您不能将它们收集到一个列表中,这基本上是所问问题的主体所抱怨的。
  • 我看到我为帖子选择了不好的例子。考虑类型类的情况(与Show 不同)有多个方法。该解决方案除了其令人不快的形式(手写地图并不酷)之外,不适用于此。
  • 想出一个你想翻译成Haskell的Java具体例子怎么样。这将有助于引导这场讨论。
  • @ArtemPelenitsyn 当有多种方法时,可以用一种自定义数据类型表示这些方法,每个方法一个字段。
【解决方案2】:

HList 风格的解决方案可以工作,但如果您只需要使用受约束的存在项列表并且不需要其他 HList 机制,则可以降低复杂性。

这是我在existentialist package 中处理此问题的方法:

{-# LANGUAGE ConstraintKinds, ExistentialQuantification, RankNTypes #-}

data ConstrList c = forall a. c a => a :> ConstrList c
                  | Nil
infixr :>

constrMap :: (forall a. c a => a -> b) -> ConstrList c -> [b]
constrMap f (x :> xs) = f x : constrMap f xs
constrMap f Nil       = []

然后可以这样使用:

example :: [String]
example
  = constrMap show
              (( 'a'
              :> True
              :> ()
              :> Nil) :: ConstrList Show)

如果您有一个很大的列表,或者如果您必须对受约束的存在项列表进行大量操作,这可能会很有用。

使用这种方法,您也不需要在类型(或元素的原始类型)中编码列表的长度。根据情况,这可能是好事也可能是坏事。如果你想保留所有原始类型信息,HList 可能是要走的路。

此外,如果(如 Show 的情况)只有一个类方法,我建议的方法是将该方法直接应用于列表中的每个项目,如 ErikR 的答案或 phadej 的第一种技术回答。

听起来实际问题比Show-able 值的列表更复杂,因此如果没有更具体的信息,很难给出明确的建议来确定哪些是最合适的。

不过,其中一种方法可能效果很好(除非可以简化代码本身的架构,以免一开始就遇到问题)。

泛化到包含在更高种类类型中的存在

这可以推广到像这样的更高种类:

data AnyList c f = forall a. c a => f a :| (AnyList c f)
                 | Nil
infixr :|

anyMap :: (forall a. c a => f a -> b) -> AnyList c f -> [b]
anyMap g (x :| xs) = g x : anyMap g xs
anyMap g Nil       = []

使用它,我们可以(例如)创建具有Show-able 结果类型的函数列表。

example2 :: Int -> [String]
example2 x = anyMap (\m -> show (m x))
                    (( f
                    :| g
                    :| h
                    :| Nil) :: AnyList Show ((->) Int))
  where
    f :: Int -> String
    f = show

    g :: Int -> Bool
    g = (< 3)

    h :: Int -> ()
    h _ = ()

我们可以通过定义看到这是一个真正的概括:

type ConstrList c = AnyList c Identity

(>:) :: forall c a. c a => a -> AnyList c Identity -> AnyList c Identity
x >: xs  = Identity x :| xs
infixr >:

constrMap :: (forall a. c a => a -> b) -> AnyList c Identity -> [b]
constrMap f (Identity x :| xs) = f x : constrMap f xs
constrMap f Nil                = []

这允许本文第一部分中的原始example 使用这个新的、更通用的公式工作,除了将:&gt; 更改为&gt;: 之外,现有example 代码没有任何更改(即使是这个小的更改可能可以通过模式同义词来避免。但我并不完全确定,因为我没有尝试过,有时模式同义词会以我不完全理解的方式与存在量化相互作用)。

【讨论】:

  • 不错!我最喜欢你的回答,但你能解释一下,如何将此解决方案推广到 [Int -> Showable] 等函数列表?
  • @ArtemPelenitsyn 我在回答中添加了一种方法
  • @ArtemPelenitsyn 我不得不说,我有点好奇你对此的特殊应用是什么。尽管我过去花了一些时间让这种东西发挥作用,但我从来没有真正将它用于任何事情,我一直想知道它是否有实际应用。
  • 最初的目的是开发一个小型演示框架,向学生展示 Haskell 中各种自动推导机制如何工作。举个例子DerivingFoldableDerivingFunctor。我想列出一个简单的算法列表,我们可以从这些算法中免费获得,比如sumfmap。但问题是算法返回不同类型的值。为了演示的目的,我需要这些值是可显示的。如果我向这个应用程序添加 GUI,我可能希望他们共享一些其他的 GUI 感知类。
【解决方案3】:

如果您真的非常想要,您可以使用异构列表。这种方法对 Show 确实没有用,因为它只有一个方法,你所能做的就是应用它,但如果你的类有多个方法,这可能很有用。

{-# LANGUAGE PolyKinds, KindSignatures, GADTs, TypeFamilies
   , TypeOperators, DataKinds, ConstraintKinds, RankNTypes, PatternSynonyms  #-} 

import Data.List (intercalate)
import GHC.Prim (Constraint)

infixr 5 :&
data HList xs where 
  None :: HList '[] 
  (:&) :: a -> HList bs -> HList (a ': bs) 

-- | Constraint All c xs holds if c holds for all x in xs
type family All (c :: k -> Constraint) xs :: Constraint where 
  All c '[] = () 
  All c (x ': xs) = (c x, All c xs) 

-- | The list whose element types are unknown, but known to satisfy
--   a class predicate. 
data CList c where CL :: All c xs => HList xs -> CList c  

cons :: c a => a -> CList c -> CList c
cons a (CL xs) = CL (a :& xs) 

empty :: CList c 
empty = CL None 

uncons :: (forall a . c a => a -> CList c -> r) -> r -> CList c -> r 
uncons _ n (CL None) = n 
uncons c n (CL (x :& xs)) = c x (CL xs) 

foldrC :: (forall a . c a => a -> r -> r) -> r -> CList c -> r 
foldrC f z = go where go = uncons (\x -> f x . go) z 

showAll :: CList Show -> String 
showAll l = "[" ++ intercalate "," (foldrC (\x xs -> show x : xs) [] l) ++ "]" 

test = putStrLn $ showAll $ CL $ 
  1 :& 
  'a' :& 
  "foo" :& 
  [2.3, 2.5 .. 3] :& 
  None 

【讨论】:

    【解决方案4】:

    您可以创建自己的运算符来减少语法噪音:

    infixr 5 <:
    
    (<:) :: Show a => a -> [String] -> [String]
    x <: l = show x : l
    

    所以你可以这样做:

    λ > (1 :: Int) <: True <: "abs" <: []
    ["1","True","\"abs\""]
    

    这不是[1 :: Int, True, "abs"],但不会更长。

    很遗憾,您无法将 [...] 语法与 RebindableSyntax 重新绑定。


    另一种方法是使用HList 并保留所有类型信息,即没有向下转换,没有向上转换:

    {-# LANGUAGE ConstraintKinds       #-}
    {-# LANGUAGE DataKinds             #-}
    {-# LANGUAGE GADTs                 #-}
    {-# LANGUAGE PolyKinds             #-}
    {-# LANGUAGE TypeFamilies          #-}
    {-# LANGUAGE TypeOperators         #-}
    {-# LANGUAGE UndecidableInstances  #-}
    
    import GHC.Exts (Constraint)
    
    infixr 5 :::
    
    type family All (c :: k -> Constraint) (xs :: [k]) :: Constraint where
      All c '[]       = ()
      All c (x ': xs) = (c x, All c xs)
    
    data HList as where
      HNil :: HList '[]
      (:::) :: a -> HList as -> HList (a ': as)
    
    instance All Show as => Show (HList as) where
      showsPrec d HNil       = showString "HNil"
      showsPrec d (x ::: xs) = showParen (d > 5) (showsPrec 5 x)
                             . showString " ::: "
                             . showParen (d > 5) (showsPrec 5 xs)
    

    毕竟:

    λ *Main > (1 :: Int) ::: True ::: "foo" ::: HNil
    1 ::: True ::: "foo" ::: HNil
    
    λ *Main > :t (1 :: Int) ::: True ::: "foo" ::: HNil
    (1 :: Int) ::: True ::: "foo" ::: HNil
      :: HList '[Int, Bool, [Char]]
    

    异构列表有多种编码方式,HList中的一种,还有generics-sopNP I xs。这取决于您在更大范围内要达到的目标,如果这是您需要的这种保留所有类型的方法。

    【讨论】:

      【解决方案5】:

      我会这样做:

      newtype Strings = Strings { getStrings :: [String] }
      
      newtype DiffList a = DiffList { getDiffList :: [a] -> [a] }
      
      instance Monoid (DiffList a) where
          mempty                          = DiffList id
          DiffList f `mappend` DiffList g = DiffList (f . g)
      
      class ShowList a where
          showList' :: DiffList String -> a
      
      instance ShowList Strings where
          showList' (DiffList xs) = Strings (xs [])
      
      instance (Show a, ShowList b) => ShowList (a -> b) where
          showList' xs x = showList' $ xs `mappend` DiffList (show x :)
      
      showList = showList' mempty
      

      现在,您可以创建一个ShowList,如下所示:

      myShowList = showList 1 "blah" 3.14
      

      您可以使用getStrings 获取字符串列表,如下所示:

      myStrings = getStrings myShowList
      

      这是发生了什么:

      1. ShowList a =&gt; a 类型的值可以是:

        1. Strings newtype 包装器包装的字符串列表。
        2. 或从Show 实例到ShowList 实例的函数。
      2. 这意味着函数 showList 是一个可变参数函数,它接受任意数量的可打印值,并最终返回包装在 Strings 新类型包装器中的字符串列表。

      3. 您最终可以对 ShowList a =&gt; a 类型的值调用 getStrings 以获得最终结果。此外,您不需要自己进行任何显式类型强制。

      优点:

      1. 您可以随时向列表中添加新元素。
      2. 语法简洁。您不必在每个元素前手动添加show
      3. 它不使用任何语言扩展。因此,它也适用于 Haskell 98。
      4. 您将获得两全其美、类型安全和出色的语法。
      5. 使用差异列表,您可以在线性时间内构造结果。

      有关具有可变参数的函数的更多信息,请阅读以下问题的答案:

      How does Haskell printf work?

      【讨论】:

      • 是否可以重新安排一些事情以避免运行时reverse?也许是某种类型索引技巧?
      • @dfeuer 您可以使用差异列表而不是字符串。因此,showList' :: ShowList a =&gt; DiffList String -&gt; a。我会更新我的答案。
      • @dfeuer Awww。感谢您的编辑。现在我的代码有一个笑脸。 :)
      【解决方案6】:

      我的答案与 ErikR 的基本相同:最能体现您要求的类型是 [String]。但我会更深入地探讨我认为可以证明这个答案的逻辑。关键就在这个问题的引用中:

      [...] 具有一个共同属性的东西,即它们可以变成字符串。

      我们称这种类型为Stringable。但现在关键的观察结果是:

      也就是说,如果您上面的语句是Stringable 类型的整个规范,那么有一对具有这些签名的函数:

      toString :: Stringable -> String
      toStringable :: String -> Stringable
      

      ...这样两个函数是相反的。当两种类型同构时,任何使用其中一种类型的程序都可以根据另一种类型重写,而不会对其语义进行任何更改。所以Stringable 不允许你做任何String 不允许你做的事情!

      更具体而言,重点是无论如何都可以保证这种重构工作:

      1. 在程序中将对象转换为 Stringable 并将其粘贴到 [Stringable] 的每个点,将对象转换为 String 并将其粘贴到 [String]
      2. 在程序中的每一点,通过应用toString 来使用Stringable,您现在可以消除对toString 的调用。

      请注意,此参数泛化到比Stringable 更复杂的类型,具有许多“方法”。例如,“可以转换为StringInt 的事物”的类型与(String, Int) 同构。 “可以转换为String 或将它们与Foo 组合生成Bar 的事物类型”与(String, Foo -&gt; Bar) 同构。等等。基本上,这种逻辑会导致其他答案提出的“方法记录”编码。

      我认为从中吸取的教训如下:你需要一个规范更丰富,而不仅仅是“可以变成一个字符串”,以证明使用你所使用的任何机制是合理的 因此,例如,如果我们将 Stringable 值可以向下转换添加到原始类型的要求,那么存在类型现在可能变得合理:

      {-# LANGUAGE GADTs #-}
      
      import Data.Typeable
      
      data Showable = Showable
          Showable :: (Show a, Typeable a) => a -> Stringable
      
      downcast :: Typeable a => Showable -> Maybe a
      downcast (Showable a) = cast a
      

      这个Showable 类型不与String 同构,因为Typeable 约束允许我们实现downcast 函数,该函数允许我们区分产生相同字符串的不同Showables。 A richer version of this idea can be seen in this "shape example" Gist.

      【讨论】:

        【解决方案7】:

        您可以将部分应用的函数存储在列表中。

        假设我们正在构建一个可以相交的具有不同形状的光线追踪器。

        data Sphere = ...
        data Triangle = ...
        
        data Ray = ...
        data IntersectionResult = ...
        
        class Intersect t where
              intersect :: t -> Ray -> Maybe IntersectionResult
        
        instance Intersect Sphere where ...
        instance Intersect Triangle where ...
        

        现在,我们可以部分应用intersect 来获取Ray -&gt; Maybe IntersectionResult 的列表,例如:

        myList :: [(Ray -> Maybe IntersectionResult)]
        myList = [intersect sphere, intersect triangle, ...]
        

        现在,如果你想得到所有的交点,你可以这样写:

        map ($ ray) myList -- or map (\f -> f ray) myList
        

        这可以稍微扩展以处理具有多个功能的接口,例如,如果您希望能够获得某种形状:

        class ShapeWithSomething t where
                getSomething :: t -> OtherParam -> Float
        
        data ShapeIntersectAndSomething = ShapeIntersectAndSomething {
                  intersect :: Ray -> Maybe IntersectionResult,
                  getSomething :: OtherParam -> Float}
        

        我不知道这种方法的开销。我们需要为接口的每个函数存储指向函数的指针和指向形状的指针和this,这与OO语言中通常使用的共享vtable相比是很多的。我不知道 GHC 是否能够对此进行优化。

        【讨论】:

        • 这种方法对于创建列表仍然很复杂。从我的角度来看,这只是玩具示例的问题,您的列表很短并且是手写的,但我认为在您的列表来自不同的函数调用的生产代码中这不是问题。
        【解决方案8】:

        问题的核心是:你想在运行时调度(读取选择调用哪个函数),这取决于对象的“类型”是什么。在 Haskell 中,这可以通过将数据包装成 sum 数据类型(这里称为 ShowableInterface)来实现:

        data ShowableInterface = ShowInt Int | ShowApple Apple | ShowBusiness Business
        
        instance Show ShowableInterface where
           show (ShowInt i)      = show i
           show (ShowApple a)    = show a
           show (ShowBusiness b) = show b  
        
        list=[ShowInt 2, ShowApple CrunchyGold, ShowBusiness MoulinRouge]
        
        show list
        

        将对应于 Java 中的类似内容:

        class Int implements ShowableInterface
        {
           public show {return Integer.asString(i)};
        }
        class Apple implements ShowableInterface
        {
           public show {return this.name};
        }
        class ShowBusiness implements ShowableInterface
        {
           public show {return this.fancyName};
        }
        
        List list = new ArrayList (new Apple("CrunchyGold"), 
                                   new ShowBusiness("MoulingRouge"), new Integer(2));
        

        因此,在 Haskell 中,您需要将内容显式包装到 ShowableInterface 中,而在 Java 中,这种包装是在对象创建时隐式完成的。

        感谢 #haskell IRC 在一年前左右向我解释了这一点。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多