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

323 Views Asked by At

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
3

There are 3 best solutions below

0
On BEST ANSWER

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.

0
On

TL;DR:

Copy the prelude's way and use showList_ as a class function to generate instances for lists so you can override the definition for String.

Caveat
For the record, wowofbob's answer using a newtype wrapper is the simple, clean solution I would use in real life, but I felt it was instructive to also look at some of how the Prelude does this.

intercalate commas by default

The way this is done in the prelude is to make the Show class have a function for showing lists, with a default definition that you can override.

import Data.List (intercalate)

I'll use intercalate :: [a] -> [[a]] -> [a] to put commas in between stuff:

ghci> intercalate "_._" ["intercalate","works","like","this"]
"intercalate_._works_._like_._this"

Make a showList_ class function, and default to show and comma-separated lists.

So now the class with the default implementation of the showList function and, importantly, a default show_ implementation that just uses the ordinary show function. To be able to use that, we have to insist that the type is already in the Show typeclass, but that's OK as far as I understand you.

class Show a => Show_ a where
  show_ :: a -> String
  showList_ :: [a] -> String

  show_ = show
  showList_ xs = '[' : intercalate ", " (map show_ xs) ++ "]"

The real Show class uses functions of type String -> String instead of String directly for efficiency reasons, and a precedence argument to control the use of brackets, but I'll skip all that for simplicity.

Automatically make instances for lists

Now we can use the showList function to provide an instance for lists:

instance Show_ a => Show_ [a] where
   show_ xs = showList_ xs

The Show a => superclass makes instances super-easy

Now we come to some instances. Because of our default show_ implementation, we don't need to do any actual programming unless we want to override the default, which we'll do for Char, because String ~ [Char].

instance Show_ Int
instance Show_ Integer
instance Show_ Double

instance Show_ Char where
   show_ c = [c] -- so show_ 'd' = "d". You can put show_ = show if you want "'d'"
   showList_ = id -- just return the string

In practice:

Now that's not much use to hide " from your output in ghci, because the default show function is used for that, but if we use putStrLn, the quotes disappear:

put :: Show_ a => a -> IO ()
put = putStrLn . show_
ghci> show "hello"
"\"hello\""
ghci> show_ "hello"
"hello"
ghci> put "hello"
hello
ghci> put [2,3,4]
[2, 3, 4]
ghci> 
0
On

You can wrap it into newtype and make custom Show instance for it:

newtype PrettyString = PrettyString { toString :: String }

instance Show PrettyString where
  show (PrettyString s) = "$$" ++ s ++ "$$" -- for example

And then use it like below:

main = getLine >>= print . PrettyString