Search code examples
haskellgenericstemplate-haskell

Get name of the constructor


I'm trying to find a way to be able to get name of my datatype constructor as String

data Test
  = Foo
    { a :: Int
    , b :: Int
    }
  | Bar
    { a :: Int
    }
  | Lol
  | Lel String

I'm searching something in form of name :: Constructor Test -> String which can be used like:

name Lol -- "Lol"
name Foo -- "Foo"
name Lel -- "Lel"

Closest what I was able to achieve was:

module Main where

import Data.Typeable
import Data.Data

data Test
  = Foo
    { a :: Int
    , b :: Int
    }
  | Bar
    { a :: Int
    }
  | Lol
  | Lel String
  deriving (Show, Data, Typeable)


main :: IO ()
main = do
  print $ toConstr Lol
  print $ toConstr $ Bar undefined
  print $ toConstr $ Foo undefined undefined

but toConstr excepts object as a argument instead of the constructor :/


Solution

  • Note that the constructors themselves have types:

    Foo :: Int -> Int -> Test
    Bar :: Int -> Test
    Lol :: Test
    Lel :: Strinng -> Test
    

    so you're asking for a function name that can take a constructor whose type matches any one of these "patterns" to produce a String. If you wrote down the type signature for name, it would need to look something like:

    name :: (a1 -> a2 -> ... -> an -> Test) -> String
    

    or, if we wanted to use it with any object, not just Test, something like:

    name :: (a1 -> a2 -> ... -> an -> finalObject) -> String
    

    where the number of a types depends on the arity of the constructor.

    There's no straightforward way of writing such a function in Haskell. In fact, it's impossible in "plain" Haskell. However, with some extensions, it can be done using some type class trickery.

    The extensions needed are:

    {-# LANGUAGE DeriveDataTypeable #-}
    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE UndecidableInstances #-}
    

    The idea is to introduce a type class for the name function:

    class Name a where
        name :: a -> String
    

    and then introduce an instance that handles the case where a still needs arguments by supplying undefined to reduce the argument count by one:

    instance Name (r -> a) where
        name f = name (f undefined)
    

    This instance will be used recursively. When we call name Foo, it'll be used to reduce this to name (Foo undefined) and then used again to reduce it to name (Foo undefined undefined). Since this final object doesn't match the pattern r -> a, we'll be ready to use the default instance:

    instance Name a where
        name = show . toConstr
    

    This code won't work as-is. We need to add some constraints in appropriate places and use an OVERLAPPING pragma to handle these overlapping instances, but the final definition of the type class and its instances is:

    class Name a where
      name :: a -> String
    instance {-# OVERLAPPING #-} Name a => Name (r -> a) where
      name f = name (f undefined)
    instance (Data a) => Name a where
      name = show . toConstr
    

    This works fine:

    λ> name Foo
    "Foo"
    λ> name Bar
    "Bar"
    λ> name Lol
    "Lol"
    λ> name Lel
    "Lel"
    

    However, now that you have this function, I think you will discover that it's incredibly difficult to use in a real program.

    Anyway, the full code follows. Note that modern versions of GHC don't need deriving Typeable, so you can leave it out.

    {-# LANGUAGE DeriveDataTypeable #-}
    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE UndecidableInstances #-}
    
    module Constructor where
    
    import Data.Data
    
    data Test
      = Foo
        { a :: Int
        , b :: Int
        }
      | Bar
        { a :: Int
        }
      | Lol
      | Lel String
      deriving (Show, Data)
    
    class Name a where
      name :: a -> String
    instance {-# OVERLAPPING #-} Name a => Name (r -> a) where
      name f = name (f undefined)
    instance (Data a) => Name a where
      name = show . toConstr
    
    main = do
      print $ name Foo
      print $ name Bar
      print $ name Lol
      print $ name Lel