我想建立一个包含一个共同属性的不同东西的列表,即它们可以变成字符串.面向对象的方法很简单:定义接口Showable并使感兴趣的类实现它.当你不能改变类时,第二点原则上可能是一个问题,但让我们假装情况并非如此.然后你创建一个Showables 列表并用这些类的对象填充它而不会产生任何额外的噪音(例如,通常隐式地进行向上转换).这里给出了Java中的概念证明.
我的问题是我在Haskell中有什么选择?下面我讨论一些我尝试过但并不能让我满意的方法.
方法1:existensials.工作但丑陋.
{-# LANGUAGE ExistentialQuantification #-}
data Showable = forall a. Show a => Sh a
aList :: [Showable]
aList = [Sh (1 :: Int), Sh "abc"]
Run Code Online (Sandbox Code Playgroud)
我这里的主要缺点是Sh填写清单时的必要性.这非常类似于在OO语言中隐式完成的向上操作.
更一般地说,Showable已经在语言Show类型类中的虚拟包装器在我的代码中增加了额外的噪声.不好.
方法2:impredicatives.期望但不起作用.
对我来说这个列表最直接的类型和我真正想要的是:
{-# LANGUAGE ImpredicativeTypes #-}
aList :: [forall a. Show a => a]
aList = [(1 :: Int), "abc"]
Run Code Online (Sandbox Code Playgroud)
除此之外(正如我所听到的)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
Run Code Online (Sandbox Code Playgroud)
和相同的错误"abc".(注意1的类型签名:没有它我收到更奇怪的消息:) Could not deduce (Num a) arising from the literal ‘1’.
方法3:Rank-N与某种功能列表(差异列表?)一起输入.
而不是有问题的ImpredicativeTypes人可能更喜欢更稳定和广泛接受RankNTypes.这基本上意味着:将所需forall a. Show a => a的类型构造函数(即[])移动到普通函数类型.因此,我们需要将列表表示为普通函数.我几乎听不到有这样的陈述.我听说过的是差异清单.但是在Dlist包装中,主要类型是好的,data所以我们回到了预测.我没有进一步研究这一行,因为我怀疑它可能会产生比方法1更冗长的代码.但如果你认为它不会,请给我一个例子.
一句话:你如何在Haskell中攻击这样的任务?你能提供比OO语言更简洁的解决方案(特别是代替填写列表 - 请参阅方法1中的代码注释)?你能评论一下上面列出的方法有多相关吗?
UPD(基于第一条评论):为了便于阅读,问题当然是简化的.真正的问题更多的是如何存储共享相同类型类的东西,即稍后可以通过多种方式处理(Show只有一种方法,但其他类可以有多种方法).这会导致show在填写列表时建议应用方法的解决方案.
Eri*_*ikR 35
由于在Haskell中评估是懒惰的,那么如何创建实际字符串列表呢?
showables = [ show 1, show "blah", show 3.14 ]
Run Code Online (Sandbox Code Playgroud)
Dav*_*vid 17
该HList式的解决方案,将工作,但它是可以减少复杂性,如果你只需要与约束existentials的列表来工作,你不需要其他HList机械.
这是我在我的existentialist包中处理这个问题的方法:
{-# 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 = []
Run Code Online (Sandbox Code Playgroud)
这可以像这样使用:
example :: [String]
example
= constrMap show
(( 'a'
:> True
:> ()
:> Nil) :: ConstrList Show)
Run Code Online (Sandbox Code Playgroud)
如果您有一个大型列表,或者如果您必须对受约束的存在列表进行大量操作,则可能很有用.
使用此方法,您也不需要在类型(或元素的原始类型)中编码列表的长度.根据情况,这可能是好事还是坏事.如果您想保留所有原始类型信息,那么HList可能就是这样.
此外,如果(如果是Show)只有一个类方法,我建议的方法是将该方法直接应用于列表中的每个项目,如ErikR的答案或phadej答案中的第一个技术.
听起来实际问题比仅仅列出的Show值更复杂,因此如果没有更具体的信息,很难明确地建议哪些具体是最合适的.
其中一种方法可能会很好用(除非代码本身的体系结构可以简化,以便它首先不会遇到问题).
这可以推广到更高的类型,如下所示:
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 = []
Run Code Online (Sandbox Code Playgroud)
使用它,我们可以(例如)创建一个具有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 _ = ()
Run Code Online (Sandbox Code Playgroud)
通过定义,我们可以看到这是一个真正的概括:
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 = []
Run Code Online (Sandbox Code Playgroud)
这使得原来example从这个第一部分使用这个新的,更普遍的,配方没有改变现有的工作example代码,只是改变:>来>:(甚至这个微小的变化或许能够与模式同义词是可以避免的.我不完全当然,因为我没有尝试过,有时模式同义词会以我不完全理解的方式与存在量化相互作用.
use*_*038 11
如果你真的真的想要,你可以使用异构列表.这种方法对于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
Run Code Online (Sandbox Code Playgroud)
您可以创建自己的运算符以减少语法噪音:
infixr 5 <:
(<:) :: Show a => a -> [String] -> [String]
x <: l = show x : l
Run Code Online (Sandbox Code Playgroud)
所以你可以这样做:
? > (1 :: Int) <: True <: "abs" <: []
["1","True","\"abs\""]
Run Code Online (Sandbox Code Playgroud)
这[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)
Run Code Online (Sandbox Code Playgroud)
毕竟:
? *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]]
Run Code Online (Sandbox Code Playgroud)
编码异构列表有多种方式,在HList一个,也generics-sop有NP I xs.这取决于你想要在更大的环境中实现什么,如果这是保留 - 所有类型的方法是你需要的.
我会做这样的事情:
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
Run Code Online (Sandbox Code Playgroud)
现在,您可以创建ShowList如下:
myShowList = showList 1 "blah" 3.14
Run Code Online (Sandbox Code Playgroud)
您可以使用getStrings以下方法返回字符串列表:
myStrings = getStrings myShowList
Run Code Online (Sandbox Code Playgroud)
这是发生了什么:
类型的值ShowList a => a可以是:
Stringsnewtype包装器中的字符串列表.Show到实例的函数ShowList.这意味着该函数showList是一个可变参数函数,它接受任意数量的可打印值,并最终返回包含在Stringsnewtype包装器中的字符串列表.
您最终可以调用getStrings类型的值ShowList a => a来获得最终结果.此外,您不需要自己做任何明确的类型强制.
好处:
show在每个元素前面手动添加.有关具有可变参数的函数的更多信息,请阅读以下问题的答案:
我的答案与ErikR基本相同:最能体现您要求的类型[String].但是我会更多地了解我认为可以证明这个答案的逻辑.问题的关键在于引用:
[...]具有一个共同属性的东西,即它们可以变成字符串.
我们称之为这种类型Stringable.但现在关键的观察是:
Stringable是同构的String!也就是说,如果您的上述陈述是该类型的整个规范Stringable,那么有一对具有这些签名的函数:
toString :: Stringable -> String
toStringable :: String -> Stringable
Run Code Online (Sandbox Code Playgroud)
......这样两个函数都是反转的.当两种类型是同构的时,任何使用其中任何一种类型的程序都可以根据其他类型重写,而不会对其语义进行任何更改.所以Stringable不要让你做任何不能让你做的事情String!
更具体地说,重点是无论如何,这种重构都可以保证:
Stringable,并坚持说成一个[Stringable],把对象变成一个String,并坚持认为成[String].Stringable通过应用toString它来消耗a ,你现在可以消除对它的调用toString.请注意,此参数概括为比Stringable使用许多"方法" 更复杂的类型.因此,例如,"你可以变成一个String或一个Int"的东西的类型是同构的(String, Int)."你可以变成一个String或者用它们组合起来Foo产生一个Bar"的东西的类型是同构的(String, Foo -> 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
Run Code Online (Sandbox Code Playgroud)
这种Showable类型不是同构的String,因为Typeable约束允许我们实现downcast允许我们区分Showable产生相同字符串的不同s 的函数. 在这个"形状示例"要点中可以看到这个想法的更丰富版本.