我有一个多态函数,如:
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对这个问题的所有三个答案,因为他们都明白我真正在问的是什么。我会说:
话虽如此,它们都是很好的答案,上面的分类是一个粗略的简化。我还更新了问题标题以更好地代表我的问题(谢谢再次感谢您扩大我的视野!
答案 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
的实例,那么就有办法了。如果你真的不需要这个,那么不要使用这个代码!如果您确实需要执行此操作,则必须使用名为UndecidableInstances
和OverlappingInstances
语言扩展名(以及名称较小的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 String
和GraphvizLabel (Map k v)
)保持不变;您刚刚将Int
和Char
个案合并到GraphvizLabel a
案例中。请记住,UndecidableInstances
正是它所说的:编译器无法告诉实例是否可检查或者是否会使typechecker循环!在这种情况下,我有理由相信这里的所有内容实际上都是可判定的(但如果有人注意到我错了,请让我知道)。尽管如此,应始终谨慎使用UndecidableInstances
。