showables列表:OOP击败Haskell?

Art*_*syn 40 haskell

我想建立一个包含一个共同属性的不同东西的列表,即它们可以变成字符串.面向对象的方法很简单:定义接口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)

  • @arrowd这不是很好的类型:在应用`show`之前你不能将它们收集到一个列表中,这实际上是问题的主体所抱怨的. (11认同)
  • 如何在Java中提出一个你想要翻译成Haskell的具体例子.这将有助于指导这一讨论. (5认同)
  • @ArtemPelenitsyn当有多个方法时,可以用自定义数据类型表示那些方法,每个方法有一个字段. (3认同)

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)


pha*_*dej 9

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

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-sopNP I xs.这取决于你想要在更大的环境中实现什么,如果这是保留 - 所有类型的方法是你需要的.


Aad*_*hah 7

我会做这样的事情:

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)

这是发生了什么:

  1. 类型的值ShowList a => a可以是:

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

  3. 您最终可以调用getStrings类型的值ShowList a => a来获得最终结果.此外,您不需要自己做任何明确的类型强制.

好处:

  1. 您可以随时在列表中添加新元素.
  2. 语法简洁.您不必show在每个元素前面手动添加.
  3. 它不使用任何语言扩展.因此,它也适用于Haskell 98.
  4. 您可以充分利用这两个世界,输入安全性和良好的语法.
  5. 使用差异列表,您可以在线性时间内构造结果.

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

Haskell printf如何工作?

  • @dfeuer Awww.谢谢你的编辑.现在我的代码有一个笑脸.:) (4认同)

Lui*_*las 5

我的答案与ErikR基本相同:最能体现您要求的类型[String].但是我会更多地了解我认为可以证明这个答案的逻辑.问题的关键在于引用:

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

我们称之为这种类型Stringable.但现在关键的观察是:

  • Stringable同构String!

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

toString :: Stringable -> String
toStringable :: String -> Stringable
Run Code Online (Sandbox Code Playgroud)

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

更具体地说,重点是无论如何,这种重构都可以保证:

  1. 在你的程序中你把一个对象变成每一个点Stringable,并坚持说成一个[Stringable],把对象变成一个String,并坚持认为成[String].
  2. 在程序的每个点上,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 的函数. 在这个"形状示例"要点中可以看到这个想法的更丰富版本.