haskelltypespattern-matchingpattern-guards

How do I let a function in Haskell depend on the type of its argument?


I tried to write a variation on show that treats strings differently from other instances of Show, by not including the " and returning the string directly. But I don't know how to do it. Pattern matching? Guards? I couldn't find anything about that in any documentation.

Here is what I tried, which doesn't compile:

show_ :: Show a => a -> String
show_ (x :: String) = x
show_ x             = show x

Solution

  • If possible, you should wrap your values of type String up in a newtype as @wowofbob suggests.

    However, sometimes this isn't feasible, in which case there are two general approaches to making something recognise String specifically.

    The first way, which is the natural Haskell approach, is to use a type class just like Show to get different behaviour for different types. So you might write

    class Show_ a where
        show_ :: a -> String
    

    and then

    instance Show_ String where
        show_ x = x
    
    instance Show_ Int where
        show_ x = show x
    

    and so on for any other type you want to use. This has the disadvantage that you need to explicitly write out Show_ instances for all the types you want.

    @AndrewC shows how you can cut each instance down to a single line, but you'll still have to list them all explicitly. You can in theory work around this, as detailed in this question, but it's not pleasant.

    The second option is to get true runtime type information with the Typeable class, which is quite short and simple in this particular situation:

    import Data.Typeable
    
    [...]
    
    show_ :: (Typeable a, Show a) => a -> String
    show_ x =
        case cast x :: Maybe String of
            Just s -> s
            Nothing -> show x
    

    This is not a natural Haskell-ish approach because it means callers can't tell much about what the function will do from the type.

    Type classes in general give constrained polymorphism in the sense that the only variations in behaviour of a particular function must come from the variations in the relevant type class instances. The Show_ class gives some indication what it's about from its name, and it might be documented.

    However Typeable is a very general class. You are delegating everything to the specific function you are calling; a function with a Typeable constraint might have completely different implementations for lots of different concrete types.

    Finally, a further elaboration on the Typeable solution which gets closer to your original code is to use a couple of extensions:

    {-# LANGUAGE ViewPatterns, ScopedTypeVariables #-}
    import Data.Typeable
    
    [...]
    
    show_ :: (Typeable a, Show a) => a -> String
    show_ (cast -> Just (s :: String)) = s
    show_ x = show x
    

    The use of ViewPatterns allows us to write the cast inside a pattern, which may fit in more nicely with more complicated examples. In fact we can omit the :: String type constraint because the body of this cases forces s to be the result type of show_, i.e. String, anyway. But that's a little obscure so I think it's better to be explicit.