Search code examples
haskellpolymorphismabstract-data-type

Best way to implement ad-hoc polymorphism in Haskell?


I have a polymorphic function like:

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

But sometimes I want to pass it a Data.Map and do some more fancy key value conversion. I know I can't pattern match here because Data.Map is an abstract data type (according to this similar SO question), but I have been unsuccessful using guards to this end, and I'm not sure if ViewPatterns would help here (and would rather avoid them for portability).

This is more what I want:

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

But this doesn't work because M.size can't take anything other than a Data.Map.

Specifically, I am trying to modify the sl utility function in the Functional Graph Library in order to handle coloring and other attributes of edges in GraphViz output.

Update

I wish I could accept all three answers by TomMD, Antal S-Z, and luqui to this question as they all understood what I really was asking. I would say:

  • Antal S-Z gave the most 'elegant' solution as applied to the FGL but would also require the most rewriting and rethinking to implement in personal problem.
  • TomMD gave a great answer that lies somewhere between Antal S-Z's and luqui's in terms of applicability vs. correctness. It also is direct and to the point which I appreciate greatly and why I chose his answer.
  • luqui gave the best 'get it working quickly' answer which I will probably be using in practice (as I'm a grad student, and this is just some throwaway code to test some ideas). The reason I didn't accept was because TomMD's answer will probably help other people in more general situations better.

With that said, they are all excellent answers and the above classification is a gross simplification. I've also updated the question title to better represent my question (Thanks Thanks again for broadening my horizons everyone!


Solution

  • What you just explained is you want a function that behaves differently based on the type of the input. While you could use a data wrapper, thus closing the function for all time:

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

    A better way is to use type classes, thus leaving the convert function open and extensible while preventing users from inputting non-sensical combinations (ex: 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) ++ "]"
    

    In this manner you can have the instances you want used for each different data type and every time a new specialization is needed you can extend the definition of convert simply by adding another instance Convertable NewDataType where ....

    Some people might frown at the newtype wrapper and suggest an instance like:

    instance Convertable a where
        convert ...
    

    But this will require the strongly discouraged overlapping and undecidable instances extensions for very little programmer convenience.