Listables:OOP击败Haskell?

我想build立一个具有共同特性的不同事物的列表,即它们可以变成string。 面向对象的方法很简单:定义接口Showable并使感兴趣的类实现它。 第二点原则上可以是一个问题,当你不能改变类,但让我们假装不是这样的。 然后你创build一个Showable列表,并用这些类的对象填充它,而不会有任何额外的噪音(例如,上传通常是隐式的)。 Java中的概念certificate在这里给出 。

我的问题是我在Haskell有什么select? 下面我讨论一下我尝试过的方法,哪些方法不能满足我。

方法1 :existensials。 工作,但丑陋。

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

我这里的主要缺点是在填写清单时需要Sh 。 这类似于OO语言隐含的upcast操作。

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

方法2 :暗示性。 期望但不起作用。

对我来说这样一个清单最直接的types,我真正渴望的是:

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

除此之外( 据我 ImpredicativeTypesImpredicativeTypes是“脆弱的,最坏的”,它不会编译:

 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"相同的错误。 (注意types签名为1:没有它我收到更奇怪的消息: Could not deduce (Num a) arising from the literal '1' )。

方法3 :Rank-Ntypes和某种function列表(差异列表?)。

人们可能更喜欢更稳定和广泛接受的RankNTypes而不是有问题的RankNTypes 。 这基本上意味着:移动所需的forall a. Show a => a forall a. Show a => atypes的构造函数(ie [] )为普通函数types。 因此,我们需要一些列表作为普通的函数。 我刚刚听到有这样的表示。 我听到的是差异列表。 但是在Dlist包中 ,主要types是好的旧data所以我们返回到impreicatives。 我没有进一步调查这条线,因为我怀疑它可能产生比方法1更详细的代码。但是,如果你认为它不会,请给我一个例子。

底线 :你将如何攻击Haskell这样的任务? 你能给出比OO语言更简洁的解决scheme吗(特别是在填写清单的地方 – 见方法1中的代码评论)? 你能否评论上面列出的方法有多相关?

UPD (基于第一条评论):这个问题当然是为了可读性而简化的。 真正的问题是如何存储共享相同types的东西,即可以用多种方法处理( Show只有一个方法,但其他类可以有多个)。 这个因素列出了填表时适用show method的解决scheme。

HList风格的解决scheme是可行的,但是如果您只需要处理受限制的存在列表并且不需要其他HList机制,就可以降低复杂性。

以下是我在existentialist包装中处理这个问题的方法:

 {-# LANGUAGE ConstraintKinds, ExistentialQuantification, RankNTypes #-} data ConstrList c = forall a. ca => a :> ConstrList c | Nil infixr :> constrMap :: (forall a. ca => a -> b) -> ConstrList c -> [b] constrMap f (x :> xs) = fx : constrMap f xs constrMap f Nil = [] 

这可以像这样使用:

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

如果你有一个大的列表,或者如果你必须对受限制的存在列表进行大量的操作,这可能是有用的。

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

另外,如果(和Show )只有一个类方法,那么我build议的方法是将这个方法直接应用到列表中的每个项目,就像ErikR的回答或者phadej答案中的第一个技巧。

这听起来像是实际问题比Show值列表更复杂,所以很难给出明确的build议,哪些具体的信息是最合适的,而没有更具体的信息。

其中一种方法可能会运行得很好(除非代码本身的架构可以被简化,以至于不会首先遇到问题)。

推广到更高types的存在

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

 data AnyList cf = forall a. ca => fa :| (AnyList cf) | Nil infixr :| anyMap :: (forall a. ca => fa -> b) -> AnyList cf -> [b] anyMap g (x :| xs) = gx : anyMap g xs anyMap g Nil = [] 

使用这个,我们可以(例如)创build一个具有可Show结果types的函数列表。

 example2 :: Int -> [String] example2 x = anyMap (\m -> show (mx)) (( 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. ca => a -> AnyList c Identity -> AnyList c Identity x >: xs = Identity x :| xs infixr >: constrMap :: (forall a. ca => a -> b) -> AnyList c Identity -> [b] constrMap f (Identity x :| xs) = fx : constrMap f xs constrMap f Nil = [] 

这允许来自第一部分的原始example使用这个新的更一般的公式来工作,而不改变现有example代码,除了改变:>>:即使这种小的改变也可以通过模式同义词来避免我并不完全确定,因为我没有尝试过,有时候模式同义词与我不完全理解的存在量化相互作用。

既然在Haskell中评估是懒惰的,那么只要创build一个实际的string列表呢?

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

如果你确实想要,你可以使用一个异构列表。 这种方法对于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) = (cx, 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 :: ca => a -> CList c -> CList c cons a (CL xs) = CL (a :& xs) empty :: CList c empty = CL None uncons :: (forall a . ca => a -> CList c -> r) -> r -> CList c -> r uncons _ n (CL None) = n uncons cn (CL (x :& xs)) = cx (CL xs) foldrC :: (forall a . ca => a -> r -> r) -> r -> CList c -> r foldrC fz = go where go = uncons (\x -> fx . 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 

您可以创build自己的操作员来减less语法噪音:

 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并保存所有types的信息,即不需要downcasts,不需要upcasts:

 {-# 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) = (cx, 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是一个,也有NP I xs generics-sop 。 这取决于你想要在更大范围内实现什么,如果这是保留 – 所有types的方法就是你所需要的。

我会做这样的事情:

 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 

现在,你可以创build一个ShowList ,如下所示:

 myShowList = showList 1 "blah" 3.14 

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

 myStrings = getStrings myShowList 

这是发生了什么事情:

  1. typesShowList a => a可以是:

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

  3. 您最终可以调用typesShowList a => a的值的getStrings来获得最终结果。 另外,你不需要自己做任何明确的types强制。

优点:

  1. 您可以随时将新元素添加到列表中。
  2. 语法简洁。 您不必在每个元素前手动添加show
  3. 它不使用任何语言扩展。 因此,它也适用于Haskell 98。
  4. 你得到了两全其美的好处,types安全和一个很好的语法。
  5. 使用差异列表,您可以构build线性时间的结果。

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

Haskell printf如何工作?

我的答案与ErikR的基本相同:最能体现您需求的types是[String] 。 但是我会进一步探讨一下我认为这个答案是正确的逻辑。 关键在于这个问题:

有一个共同特征的东西,即它们可以变成弦。

我们来调用这个types的Stringable 。 但是现在关键的观察是这样的:

  • Stringable同构String

也就是说,如果你上面的语句是Stringabletypes的整个规范,那么有一对函数与这些签名:

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

…这两个函数是逆。 当两种types是同构的时候,任何使用这两种types的程序都可以用另一种方式来重写,而不会改变其语义。 所以Stringable不会让你做任何事情, String不会让你做!

更具体地说,重点是无论如何保证这个重构:

  1. 在你的程序中的每一个点你把一个对象变成一个Stringable并把它粘到一个[Stringable] ,把这个对象变成一个String并把它粘到一个[String]
  2. 在你的程序的每一个点上,你通过应用toString来使用一个Stringable ,你现在可以消除对toString的调用。

请注意,这个参数泛化为比Stringable更复杂的types,带有许多“方法”。 因此,例如,“可以变成StringInt ”的types同构于(String, Int) 。 “你可以变成一个String或者把它们和Foo结合起来产生一个Bar ”的types是(String, Foo -> Bar)同构的。 等等。 基本上,这个逻辑导致了其他答案所提出的“方法logging”编码。

我认为从中吸取的教训如下: 你需要一个比“可以变成一个string” 更丰富的规范来certificate你使用的任何机制。 例如,如果我们添加Stringable值可以被Stringable到原始types的要求, Stringable现在存在types可能是合理的:

 {-# 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 

这个Showabletypes与String不是同构的,因为Typeable约束允许我们实现downcast函数,该函数允许我们区分产生相同string的不同的Showable 。 这个想法的更丰富的版本可以在这个“形状的例子”中看到。

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

假设我们正在build造一个可以交叉的不同形状的射线追踪器。

 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 -> Maybe IntersectionResult的列表,例如:

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

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

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

这可以扩展一点,以处理与多个function的接口,例如,如果你想能够得到一个形状的东西:

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

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

问题的核心是:你想在运行时调度(读select哪个函数来调用),这取决于对象的“types”是什么。 在Haskell中,这可以通过将数据包装成总和数据types(这里称为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中,这个包装是隐式完成对象创build的。

一年前,信贷去了#haskell IRC向我解释这个,左右。