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
已经在语言中的东西 – Show
types类 – 在我的代码中增加了额外的噪音。 不好。
方法2 :暗示性。 期望但不起作用。
对我来说这样一个清单最直接的types,我真正渴望的是:
{-# LANGUAGE ImpredicativeTypes #-} aList :: [forall a. Show a => a] aList = [(1 :: Int), "abc"]
除此之外( 据我 ImpredicativeTypes
) 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"
相同的错误。 (注意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 => a
types的构造函数(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
这是发生了什么事情:
-
types
ShowList a => a
可以是:- 包装在
Strings
newtype包装中的Strings
列表。 - 或者从
Show
实例到ShowList
实例的ShowList
。
- 包装在
-
这意味着函数
showList
是一个可变参数函数,它接受任意数量的可打印值并最终返回包装在Strings
newtype包装器中的Strings
列表。 -
您最终可以调用types
ShowList a => a
的值的getStrings
来获得最终结果。 另外,你不需要自己做任何明确的types强制。
优点:
- 您可以随时将新元素添加到列表中。
- 语法简洁。 您不必在每个元素前手动添加
show
。 - 它不使用任何语言扩展。 因此,它也适用于Haskell 98。
- 你得到了两全其美的好处,types安全和一个很好的语法。
- 使用差异列表,您可以构build线性时间的结果。
有关可变参数函数的更多信息,请阅读以下问题的答案:
Haskell printf如何工作?
我的答案与ErikR的基本相同:最能体现您需求的types是[String]
。 但是我会进一步探讨一下我认为这个答案是正确的逻辑。 关键在于这个问题:
有一个共同特征的东西,即它们可以变成弦。
我们来调用这个types的Stringable
。 但是现在关键的观察是这样的:
-
Stringable
是同构的String
!
也就是说,如果你上面的语句是Stringable
types的整个规范,那么有一对函数与这些签名:
toString :: Stringable -> String toStringable :: String -> Stringable
…这两个函数是逆。 当两种types是同构的时候,任何使用这两种types的程序都可以用另一种方式来重写,而不会改变其语义。 所以Stringable
不会让你做任何事情, String
不会让你做!
更具体地说,重点是无论如何保证这个重构:
- 在你的程序中的每一个点你把一个对象变成一个
Stringable
并把它粘到一个[Stringable]
,把这个对象变成一个String
并把它粘到一个[String]
。 - 在你的程序的每一个点上,你通过应用
toString
来使用一个Stringable
,你现在可以消除对toString
的调用。
请注意,这个参数泛化为比Stringable
更复杂的types,带有许多“方法”。 因此,例如,“可以变成String
或Int
”的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
这个Showable
types与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向我解释这个,左右。