JKn*_*ght 12 polymorphism haskell abstract-data-type
我有一个多态函数,如:
convert :: (Show a) => a -> String
convert = " [label=" ++ (show a) ++ "]"
Run Code Online (Sandbox Code Playgroud)
但有时我想传递一个Data.Map并做一些更精彩的键值转换.我知道我不能在这里进行模式匹配,因为Data.Map是一个抽象的数据类型(根据这个类似的SO问题),但是我一直没有成功使用警卫,而且我不确定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
Run Code Online (Sandbox Code Playgroud)
但这不起作用,因为M.size不能采取除Data.Map之外的任何东西.
具体来说,我试图修改功能图库中的sl实用程序功能,以便处理GraphViz输出中边的着色和其他属性.
更新
我希望我能接受TomMD,Antal SZ和luqui对这个问题的所有三个答案,因为他们都明白我真正在问的是什么.我会说:
话虽如此,它们都是很好的答案,上面的分类是一个粗略的简化.我还更新了问题标题以更好地代表我的问题(谢谢再次感谢您扩大我的视野!
Tho*_*son 13
你刚才解释的是你想要一个基于输入类型的行为不同的函数.虽然你可以使用data包装器,因此关闭所有时间的功能:
data Convertable k a = ConvMap (Map k a) | ConvOther a
convert (ConvMap m) = ...
convert (ConvOther o) = ...
Run Code Online (Sandbox Code Playgroud)
更好的方法是使用类型类,从而使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) ++ "]"
Run Code Online (Sandbox Code Playgroud)
通过这种方式,您可以将所需的实例用于每种不同的数据类型,并且每次需要新的特化时,convert只需添加另一个即可扩展定义instance Convertable NewDataType where ....
有些人可能会对newtype包装器皱眉并建议一个例如:
instance Convertable a where
convert ...
Run Code Online (Sandbox Code Playgroud)
但这需要强烈阻止重叠和不可判定的实例扩展,以便于程序员的方便.
你可能没有问正确的事情.我将假设你有一个节点都是Maps的图形,或者你有一个节点都是其他节点的图形.如果你需要一个Maps和非地图共存的图表,那么你的问题还有更多(但这个解决方案仍然有用).在这种情况下,请参阅我的答案的结尾.
这里最干净的答案是简单地convert为不同的类型使用不同的函数,并且具有依赖于convert将其作为参数的任何类型(更高阶函数).
所以在GraphViz中(避免重新设计这个糟糕的代码)我会修改graphviz函数看起来像:
graphvizWithLabeler :: (a -> String) -> ... -> String
graphvizWithLabeler labeler ... =
...
where sa = labeler a
Run Code Online (Sandbox Code Playgroud)
然后graphviz简单地委托它:
graphviz = graphvizWithLabeler sl
Run Code Online (Sandbox Code Playgroud)
然后graphviz继续像以前一样工作,graphvizWithLabeler当你需要更强大的版本时,你就拥有了.
因此对于节点为的图Maps,请使用graphvizWithLabeler processMap2FancyKVString,否则使用graphviz.通过将相关事物作为高阶函数或类型类方法,可以尽可能地推迟该决定.
如果你需要Map在同一个图中共存s和其他东西,那么你需要找到一个节点可能存在的单一类型.这与TomMD的建议类似.例如:
data NodeType
= MapNode (Map.Map Foo Bar)
| IntNode Int
Run Code Online (Sandbox Code Playgroud)
当然,参数化为您需要的通用级别.那么你的贴标机功能应决定在每种情况下做什么.
要记住的一个关键点是Haskell没有向下转型.类型的功能foo :: a -> a无法知道传递给它的内容(在合理范围内,冷却你的喷气式小学生).因此,您尝试编写的函数无法在Haskell中表达.但正如您所看到的,还有其他方法可以完成工作,结果他们变得更加模块化.
这有没有告诉你你需要知道什么来完成你想要的?
你的问题实际上与那个问题不一样.在您链接到的问题,德里克·图尔恩有他的功能就知道花了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
Run Code Online (Sandbox Code Playgroud)
在此设置中,我们可以输出到Graphviz的所有内容都是GraphvizLabel; 该toGVItem函数引用的东西,并toGVLabel把整个事情在方括号中立即使用.(我可能已经搞砸了你想要的一些格式,但那部分只是一个例子.)然后你声明什么是实例GraphvizLabel,以及如何将它变成一个项目.该TypeSynonymInstances标志只是让我们写instance GraphvizLabel String来代替instance GraphvizLabel [Char]; 它是无害的.
现在,如果你真的需要一个实例的所有东西Show也是一个实例GraphvizLabel,那么有一种方法.如果你真的不需要这个,那么不要使用这个代码!如果你确实需要这样做,你必须带上可怕的命名UndecidableInstances和OverlappingInstances语言扩展(以及不那么神奇的命名FlexibleInstances).这样做的原因是,你必须断言一切是Show能够是GraphvizLabel-但是这是很难的编译器来告诉.例如,如果您使用此代码并toGVLabel [1,2,3]在GHCi提示符处写入,那么您将收到错误,因为它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
Run Code Online (Sandbox Code Playgroud)
请注意,您的具体案例(GraphvizLabel String和GraphvizLabel (Map k v))保持不变; 你刚刚将案件Int和Char案件分崩离析GraphvizLabel a.请记住,UndecidableInstances完全意味着它所说的:编译器无法判断实例是否可检查,或者是否会使typechecker循环!在这种情况下,我有理由相信这里的所有内容实际上都是可判定的(但如果有人注意到我错了,请告诉我).尽管如此,UndecidableInstances应始终谨慎使用.