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

在Haskell中实现ad-hoc多态性的最佳方法?,haskell,polymorphism,abstract-data-type,Haskell,Polymorphism,Abstract Data Type,我有一个多态函数,如: convert :: (Show a) => a -> String convert = " [label=" ++ (show a) ++ "]" 但有时我想给它传递一个Data.Map并进行一些更奇特的键值转换。我知道我不能在这里进行模式匹配,因为Data.Map是一种抽象数据类型(根据),但我在这方面使用卫士是不成功的,我不确定ViewPatterns在这里是否有帮助(为了便于移植,我宁愿避免使用它们) 这更是我想要的: import qualifie

我有一个多态函数,如:

convert :: (Show a) => a -> String
convert = " [label=" ++ (show a) ++ "]"
但有时我想给它传递一个Data.Map并进行一些更奇特的键值转换。我知道我不能在这里进行模式匹配,因为Data.Map是一种抽象数据类型(根据),但我在这方面使用卫士是不成功的,我不确定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

具体来说,我试图修改,以便在GraphViz输出中处理边的着色和其他属性

更新

我希望我能接受TomMD、Antal S-Z和luqui对这个问题的所有三个答案,因为他们都理解我真正的问题。我想说:

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

尽管如此,它们都是很好的答案,上面的分类是一个粗略的简化。我还更新了问题标题,以更好地表达我的问题(再次感谢大家拓宽了我的视野!

您刚才解释的是,您需要一个根据输入类型表现不同的函数。虽然您可以使用
数据
包装器,从而始终关闭该函数:

data Convertable k a = ConvMap (Map k a) | ConvOther a
convert (ConvMap m) = ...
convert (ConvOther o) = ...
更好的方法是使用类型类,从而使
convert
函数保持打开和可扩展,同时防止用户输入非感官组合(例如:
convather M.empty

通过这种方式,您可以为每个不同的数据类型使用您想要的实例,并且每次需要新的专门化时,您都可以通过添加另一个
实例可转换的新数据类型来扩展
转换的定义,其中…

有些人可能会对
newtype
包装器皱眉,并建议使用如下示例:

instance Convertable a where
    convert ...

但是,这将需要非常不鼓励的重叠和不可判定的实例扩展,以给程序员带来很少的便利。

您的问题实际上与该问题中的问题不同。在您链接的问题中,Derek Thurn有一个函数,他知道该函数采用了
设置了
,但无法进行模式匹配。在您的情况下,您'正在编写一个函数,该函数将采用任何具有
Show
实例的
a
;您无法在运行时判断所查看的类型,只能依赖于任何
Show
可使用类型的函数。如果您想让一个函数对不同的数据类型执行不同的操作,这称为即席多态性,并且在Haskell中受类型类的支持,如
Show
(这与参数多态性相反,参数多态性是指当您编写一个像
head(x:)=x
这样的函数时,该函数的类型为
head::[a]->a
;而无约束的通用
a
则是该函数的参数化原因。)因此,要想做您想做的事情,您必须创建自己的类型类,并在需要时实例化它。然而,这比通常情况要复杂一些,因为您希望将
中的所有内容隐式地显示为新类型类的一部分。这需要一些潜在危险且可能不必要的强大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
标志只允许我们编写
实例GraphvizLabel字符串,而不是
实例GraphvizLabel[Char]
;它是无害的

现在,如果你真的需要一个
Show
实例也成为
GraphvizLabel
的一个实例,有一种方法。如果你真的不需要这个,那么就不要使用这个代码!如果你真的需要这样做,你必须带上这个可怕的名字
不可判定的实例
重叠实例
la语言扩展(以及名称不太吓人的
FlexibleInstances
)。原因是您必须断言
Show
能够显示的所有内容都是
GraphvizLabel
——但这对编译器来说很难分辨。例如,如果您使用此代码并编写
toglabel[1,2,3]
在GHCi提示下,您将得到一个错误,因为
1
具有类型
Num a=>a
,并且
Char
可能是
Num
的一个实例!您必须显式指定
toGVLabel([1,2,3]:[Int])
让它发挥作用。同样,这可能是不必要的繁重机器,无法解决您的问题。相反,如果您可以限制您认为将转换为标签的内容(很可能),您可以指定这些内容!但是如果您真的想
显示
abil
{-# 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
{-# 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
graphvizWithLabeler :: (a -> String) -> ... -> String
graphvizWithLabeler labeler ... =
   ...
   where sa = labeler a
graphviz = graphvizWithLabeler sl
data NodeType
    = MapNode (Map.Map Foo Bar)
    | IntNode Int