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

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对这个问题的所有三个答案,因为他们都明白我真正在问的是什么.我会说:

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

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

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)

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


luq*_*qui 9

你可能没有问正确的事情.我将假设你有一个节点都是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中表达.但正如您所看到的,还有其他方法可以完成工作,结果他们变得更加模块化.

这有没有告诉你你需要知道什么来完成你想要的?


Ant*_*sky 8

你的问题实际上与那个问题不一样.在您链接到的问题,德里克·图尔恩有他的功能就知道花了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,那么有一种方法.如果你真的不需要这个,那么不要使用这个代码!如果你确实需要这样做,你必须带上可怕的命名UndecidableInstancesOverlappingInstances语言扩展(以及不那么神奇的命名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 StringGraphvizLabel (Map k v))保持不变; 你刚刚将案件IntChar案件分崩离析GraphvizLabel a.请记住,UndecidableInstances完全意味着它所说的:编译器无法判断实例是否可检查,或者是否会使typechecker循环!在这种情况下,我有理由相信这里的所有内容实际上都是可判定的(但如果有人注意到我错了,告诉我).尽管如此,UndecidableInstances应始终谨慎使用.