2

I have a function in Template Haskell that extracts the type information for sum of record constructors as below:

listFields :: Name -> Q ([[(String,Name,Type)]])
listFields name = do
  TyConI (DataD _ _ _ cons _) <- reify name  
  let showClause (RecC conName fields) = (map (\(x,_,t) -> (nameBase $ x,x,t)) fields)
  return $ map showClause cons

Given the type in there for a field, how do you compare equality of that type with a particular type like GHC.Base.String or Data.Text.Internal.Text? I see TypeQ in TH documentation. It builds type expression. However, I can't find any documentation on how to build a particular type like String or Text or Int so that I can use it for equality comparison? Will appreciate pointers on how to do this, especially how to get the AST for a particular type.

The reason for this question is that given record constructor, we want to convert each field to Text. However, show and pack should be called differently for String and Text types. So, need to generate different splices if the type is Text (no conversion) or String (only call pack, don't call show) or something else (call pack . show assuming Show instance exists).

Sal
  • 4,312
  • 1
  • 17
  • 26
  • Since you seem to be learning Template Haskell, I'm rather curious what resources you're using. I haven't yet been able to find decent ones to learn it myself. – dfeuer Mar 22 '16 at 17:31
  • So you can do an equality check with `==` but this is almost certainly wrong because it's just raw syntactic equivalence and doesn't normalize type synonyms even, let alone indirections through modules or whatever. You'd be better off writing an overloaded type class honestly which behaves differently only on `String` or something and only using it in your codegen module. Type equality is really hard. – daniel gratzer Mar 22 '16 at 17:33
  • 1
    @dfeuer, two main resources: [deriveShow function example](https://ocharles.org.uk/blog/guest-posts/2014-12-22-template-haskell.html) and [in-depth overview of TH](https://artyom.me/lens-over-tea-6#template-haskell). Constant experimentation in `ghci` with hackage page for [`TH`](https://hackage.haskell.org/package/template-haskell-2.10.0.0/docs/Language-Haskell-TH.html) in another browser helps a lot. AST generation in `ghci` using simple code, and understanding importance of `Name` in `TH` helped me get up to speed quickly. – Sal Mar 22 '16 at 17:52
  • @jozefg, would `Data.Typeable` approach to generate appropriate splices also work (at compile-time)? Please feel free to post solutions on how to generate alternative splices (let us say, overloaded `show` function AST for different types). I think your suggestion makes sense. – Sal Mar 22 '16 at 17:56
  • @jozefg, it seems overloading Show for few types is more elaborate undertaking as documented [here](https://wiki.haskell.org/GHC/AdvancedOverlap). I tried overloading `Show` with overlapping pragma but that failed. Perhaps you could elaborate more if you know how to do this. – Sal Mar 22 '16 at 19:32
  • 1
    @dfeuer, one more important source for learning `TH` - [this](http://stackoverflow.com/questions/16490846/haskell-using-rankntypes-to-fold-a-record-constructor) one shows how to solve the problem step-by-step – Sal Mar 23 '16 at 00:39
  • @Sal, thanks a lot for the recommendations. – dfeuer Mar 23 '16 at 00:52

2 Answers2

3

As a follow-on to the other answer, here's something that lets you write ToText without any overlapping instances. It uses my new favorite trick -- mixing closed type families over datakinds as a "choice" mechanism with typical type classes (note: not even using functional dependencies, much less overlapping instances) to synthesize the actual code:

{-# LANGUAGE TypeFamilies, DataKinds, MultiParamTypeClasses, FlexibleInstances, ScopedTypeVariables, FlexibleContexts #-}

import Data.List
import Data.Text (unpack, pack, Text)
import Data.Proxy

data ToTextMethod = TTMChar | TTMString | TTMText | TTMShow

type family ToTextHow a where
     ToTextHow Char = TTMChar
     ToTextHow String = TTMString
     ToTextHow Text = TTMText
     ToTextHow a = TTMShow

class ToTextC a b where
      toTextC :: a -> b -> Text

instance Show a => ToTextC a (Proxy TTMShow) where
      toTextC a _ = pack (show a)

instance ToTextC Char (Proxy TTMChar) where
      toTextC c _ = pack [c]

instance ToTextC String (Proxy TTMString) where
      toTextC s _ = pack s

instance ToTextC Text (Proxy TTMText) where
      toTextC t _ = t

toText :: forall a. (Show a, ToTextC a (Proxy (ToTextHow a))) => a -> Text
toText x = toTextC x (Proxy :: Proxy (ToTextHow a))

The names could probably use some work, and it might be nice to flip the arguments to toTextC, but this all works even in ghc 7.8.3.

sclv
  • 38,665
  • 7
  • 99
  • 204
  • Is there some reason you didn't use `toTextC :: a -> proxy b -> Text`? That's the usual approach, and less verbose. Also, I think you should indeed flip those arguments. The downside of this, of course, is that it's quite closed. That's also an upside. – dfeuer Mar 23 '16 at 01:24
  • It also tends to be better for the more interesting argument to an MPTC to come last (as someone reminded me recently, generalized newtype deriving is better that way). So the instance selection type should probably be the first class argument. – dfeuer Mar 23 '16 at 01:27
  • As I noted at the end "it might be nice to flip the arguments" -- i didn't do it because I started one way and didn't feel like changing it, and the answer still stands regardless. Also `toTextC` is only used internally, so it doesn't really matter. The only exported "interface" is `toText` itself. – sclv Mar 23 '16 at 01:39
1

Following recommendations of jozefg in the comments, I solved this problem by using an overloaded function with type signature a -> Text. Keeping this open for few more days to see if any one has a better suggestion.

This is my original TH splice (ghci output):

> runQ [| pack . show $ 1 ::Int|]
SigE (InfixE (Just (InfixE (Just (VarE Data.Text.pack)) (VarE GHC.Base..) 
(Just (VarE GHC.Show.show)))) (VarE GHC.Base.$) (Just (LitE (IntegerL 1))))
(ConT GHC.Types.Int)

Int gets converted to Text. However, running pack . show on String or Text will be problematic since it will add another layer of double-quotes on top of that (and doesn't make sense anyway). So, we need special handling for Show for Text, String and Char types. So, solution is to write a function toText :: a -> Text and use it in the codegen as below:

> runQ [| toText $ 1 ::Int|]
SigE (InfixE (Just (VarE ToText.toText)) (VarE GHC.Base.$) (Just (LitE (IntegerL 1)))) (ConT GHC.Types.Int)

Now, the code generation is handled by toText itself depending on the type. This is how I wrote it in ghc 7.10.3 - it takes the default code (from the first splice as shown above), and overloads it for some types - now, we have the right code in TH codegen location at compile time:

{-# LANGUAGE FlexibleInstances #-}
module ToText 
where

import Data.List
import Data.Text (unpack, pack, Text)

class ToText a where
    toText :: (Show a) => a -> Text

instance {-# OVERLAPPING #-} ToText a  where
    toText = pack . show

instance {-# OVERLAPPING #-} ToText Char where
    toText c = pack [c]

instance {-# OVERLAPPING #-} ToText String where
    toText = pack

instance {-# OVERLAPPING #-} ToText Text where
    toText = id
Sal
  • 4,312
  • 1
  • 17
  • 26
  • Ew.... Overlapping instances are nasty. Is there any way you can get away with just using a default for the `toText` method? – dfeuer Mar 22 '16 at 22:18
  • @dfeuer, yep, I agree...so far, don't know of a way to avoid overlapping instances. That is why keeping this open in the hope there is a simpler solution. – Sal Mar 22 '16 at 22:29