2

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 :/

majkrzak
  • 1,332
  • 3
  • 14
  • 30
  • 1
    Is this question somehow related to [your question from a few days ago](https://stackoverflow.com/q/59916344/126014)? If so, it may be an [XY problem](https://meta.stackexchange.com/q/66377/135815), and you might be better off asking a question closer to your actual problem. – Mark Seemann Jan 28 '20 at 15:07

1 Answers1

3

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
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • It works well, I reasembled it over generics, but can not figure out why instances are overloaping – majkrzak Jan 29 '20 at 20:11
  • Note that constraints are checked *after* instances are matched and play no role in instance selection or determination of overlap. So, the instances above overlap because `instance Name a` overlaps with every other possible instance, including `instance Name (r -> a)`. – K. A. Buhr Jan 29 '20 at 20:26