Performing type equality in template haskell

梦想的初衷 提交于 2019-12-03 21:03:41

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.

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
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!