在Haskell中实现ad-hoc多态的最佳方法?

时间:2010-10-15 17:55:23

标签: haskell polymorphism abstract-data-type

我有一个多态函数,如:

convert :: (Show a) => a -> String
convert = " [label=" ++ (show a) ++ "]"

但有时我想传递一个Data.Map并做一些更精彩的键值转换。我知道我不能在这里进行模式匹配,因为Data.Map是一个抽象的数据类型(根据this similar SO question),但是我为此目的使用了防护是不成功的,我不确定ViewPatterns是否会在这里提供帮助(并且宁愿避免它们的便携性)。

这更符合我的要求:

import qualified Data.Map as M

convert :: (Show a) => a -> String
convert a 
    | M.size \=0 = processMap2FancyKVString a -- Heres a Data.Map
    | otherwise = " [label=" ++ (show a) ++ "]" -- Probably a string

但这不起作用,因为M.size除了Data.Map之外不能采取任何其他方法。

具体来说,我正在尝试修改sl utility function in the Functional Graph Library以便处理GraphViz输出中边缘的着色和其他属性。

更新

我希望我能接受TomMD,Antal S-Z和luqui对这个问题的所有三个答案,因为他们都明白我真正在问的是什么。我会说:

  • Antal S-Z提供了应用于FGL的最优雅的解决方案,但也需要最多的重写和重新思考才能在个人问题中实施。
  • TomMD给出了一个很好的答案,介于Antal S-Z和Luqui之间的适用性与正确性之间。它也是直接的,我非常欣赏这一点以及为什么我选择了他的答案。
  • luqui给出了最好的“快速开始工作”的答案,我可能会在实践中使用(因为我是一名研究生,这只是一些一次性代码来测试一些想法)。我不接受的原因是因为TomMD的回答可能会更好地帮助其他更好的人。

话虽如此,它们都是很好的答案,上面的分类是一个粗略的简化。我还更新了问题标题以更好地代表我的问题(谢谢再次感谢您扩大我的视野!

3 个答案:

答案 0 :(得分:13)

您刚才解释的是您想要一个基于输入类型的行为不同的函数。虽然您可以使用data包装器,因此一直关闭该功能:

data Convertable k a = ConvMap (Map k a) | ConvOther a
convert (ConvMap m) = ...
convert (ConvOther o) = ...

更好的方法是使用类型类,从而使convert函数保持开放和可扩展性,同时防止用户输入非感性组合(例如:ConvOther M.empty)。

class (Show a) => Convertable a where
    convert :: a -> String

instance Convertable (M.Map k a) where
    convert m = processMap2FancyKVString m

newtype ConvWrapper a = CW a
instance Convertable (ConvWrapper a) where
    convert (CW a) = " [label=" ++ (show a) ++ "]"

通过这种方式,您可以拥有要用于每种不同数据类型的实例,并且每次需要新的专业化时,只需添加另一个convert即可扩展instance Convertable NewDataType where ...的定义。

有些人可能会对newtype包装器皱眉,并建议像:

这样的实例
instance Convertable a where
    convert ...

但这需要强烈阻止重叠和不可判定的实例扩展,以便于程序员的方便。

答案 1 :(得分:9)

你可能不会问正确的事情。我将假设你有一个节点都是Map的图表,或者你有一个节点都是其他节点的图表。如果您需要一个Map和非地图共存的图表,那么您的问题会更多(但此解决方案仍然有用)。在这种情况下,请参阅我的答案的结尾。

这里最干净的答案就是对不同类型使用不同的convert函数,并且依赖于convert的任何类型将其作为参数(更高阶函数)。

所以在GraphViz中(避免重新设计这个糟糕的代码)我会修改graphviz函数看起来像:

graphvizWithLabeler :: (a -> String) -> ... -> String
graphvizWithLabeler labeler ... =
   ...
   where sa = labeler a

然后让graphviz简单地委托给它:

graphviz = graphvizWithLabeler sl

然后graphviz继续像以前一样工作,当你需要更强大的版本时,你有graphvizWithLabeler

因此,对于节点为Maps的图表,请使用graphvizWithLabeler processMap2FancyKVString,否则请使用graphviz。通过将相关事物作为高阶函数或类型类方法,可以尽可能地推迟该决定。

如果您需要在同一个图表中共存Map个和其他内容,那么您需要找到一个节点可能存在的单一类型。这与TomMD的建议类似。例如:

data NodeType
    = MapNode (Map.Map Foo Bar)
    | IntNode Int

当然,参数化为您需要的通用级别。那么你的贴标机功能应决定在每种情况下做什么。

要记住的一个关键点是 Haskell没有向下转换foo :: a -> a类型的函数无法知道传递给它的内容(在合理范围内,让你的喷气式飞机冷静下来)。因此,您尝试编写的函数无法在Haskell中表达。但正如您所看到的,还有其他方法可以完成工作,结果更加模块化。

这是否告诉了你需要知道什么来完成你想要的?

答案 2 :(得分:8)

你的问题实际上与那个问题不一样。在您链接的问题中,Derek Thurn有一个他知道的函数占用Set a,但无法模式匹配。在您的情况下,您正在编写一个函数,该函数将采用具有a实例的任何Show;你无法分辨出你在运行时看到的是什么类型,并且只能依赖于任何Show能力类型可用的函数。如果您希望函数对不同的数据类型执行不同的操作,则称为 ad-hoc多态,并在Haskell中支持类型为Show的类。 (这与参数多态相反,当您编写类似head (x:_) = x的函数时,类型为head :: [a] -> a;无约束的通用a就是这样的参数化。)所以要做你想做的事,你必须创建自己的类型类,并在需要时实例化它。但是,它比平时稍微复杂一些,因为你想让Show的一部分隐含地成为新类型的一部分。这需要一些潜在危险且可能不必要的强大GHC扩展。相反,为什么不简化事情呢?您可以通过这种方式找出实际需要打印的类型子集。完成后,您可以按如下方式编写代码:

{-# LANGUAGE TypeSynonymInstances #-}

module GraphvizTypeclass where

import qualified Data.Map as M
import Data.Map (Map)

import Data.List (intercalate) -- For output formatting

surround :: String -> String -> String -> String
surround before after = (before ++) . (++ after)

squareBrackets :: String -> String
squareBrackets = surround "[" "]"

quoted :: String -> String
quoted = let replace '"' = "\\\""
             replace c   = [c]
         in surround "\"" "\"" . concatMap replace

class GraphvizLabel a where
  toGVItem  :: a -> String
  toGVLabel :: a -> String
  toGVLabel = squareBrackets . ("label=" ++) . toGVItem

-- We only need to print Strings, Ints, Chars, and Maps.

instance GraphvizLabel String where
  toGVItem = quoted

instance GraphvizLabel Int where
  toGVItem = quoted . show

instance GraphvizLabel Char where
  toGVItem = toGVItem . (: []) -- Custom behavior: no single quotes.

instance (GraphvizLabel k, GraphvizLabel v) => GraphvizLabel (Map k v) where
  toGVItem  = let kvfn k v = ((toGVItem k ++ "=" ++ toGVItem v) :)
              in intercalate "," . M.foldWithKey kvfn []
  toGVLabel = squareBrackets . toGVItem

在此设置中,我们可以输出到Graphviz的所有内容都是GraphvizLabel的实例; toGVItem函数引用事物,toGVLabel将整个事物放在方括号中以供立即使用。 (我可能已经搞砸了你想要的一些格式,但那部分只是一个例子。)然后你声明什么是GraphvizLabel的实例,以及如何把它变成一个项目。 TypeSynonymInstances标记只允许我们写instance GraphvizLabel String而不是instance GraphvizLabel [Char];它是无害的。

现在,如果真的需要所有并且Show实例也是GraphvizLabel的实例,那么就有办法了。如果你真的不需要这个,那么不要使用这个代码!如果您确实需要执行此操作,则必须使用名为UndecidableInstancesOverlappingInstances语言扩展名(以及名称较小的FlexibleInstances)。这样做的原因是你必须声明所有 Show能够是GraphvizLabel - 但编译器很难说。例如,如果您使用此代码并在GHCi提示符下写toGVLabel [1,2,3],则会收到错误,因为1的类型为Num a => a,而Char可能是Num的实例!您必须明确指定toGVLabel ([1,2,3] :: [Int])才能使其正常工作。同样,这可能是不必要的重型机械,可以解决您的问题。相反,如果你可以限制你认为将转换为标签的东西,这很可能,你可以改为指定那些东西!但如果您真的希望Show能够暗示GraphvizLabel能力,那么这就是您所需要的:

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances
,          UndecidableInstances, OverlappingInstances #-}

-- Leave the module declaration, imports, formatting code, and class declaration
-- the same.

instance GraphvizLabel String where
  toGVItem = quoted

instance Show a => GraphvizLabel a where
  toGVItem = quoted . show

instance (GraphvizLabel k, GraphvizLabel v) => GraphvizLabel (Map k v) where
  toGVItem  = let kvfn k v = ((toGVItem k ++ "=" ++ toGVItem v) :)
              in intercalate "," . M.foldWithKey kvfn []
  toGVLabel = squareBrackets . toGVItem

请注意,您的特定案例(GraphvizLabel StringGraphvizLabel (Map k v))保持不变;您刚刚将IntChar个案合并到GraphvizLabel a案例中。请记住,UndecidableInstances正是它所说的:编译器无法告诉实例是否可检查或者是否会使typechecker循环!在这种情况下,我有理由相信这里的所有内容实际上都是可判定的(但如果有人注意到我错了,让我知道)。尽管如此,应始终谨慎使用UndecidableInstances