我们将看看GHC 9中关于类型化模板Haskell(TTH)的变化,以及如何使用 th-compat库来编写可以在GHC 8和GHC 9上使用的TTH代码。
类型化Template Haskell规范的变化
template-haskell 包在2.17.0.0版本和GHC 9.0版本中根据Make Q (TExp a)改变为一个新的类型建议,其中类型化表达式quasi-quoter ([|| ... ||])现在返回一个不同的数据类型。
在新的规范下,类型化的引号现在返回Quote m => Code m a ,而不是Q (TExp a) ,并且类型化的拼接也期待这个新类型。这对现有的代码来说是一个突破性的变化,以GHC 9.0为目标的代码库要么需要调整他们的TTH代码,要么使用另一种方法来兼容。
Code 新类型只是一个围绕m (TExp a) 的包装,定义与此类似。
newtype Code m a = Code (m (TExp a))
实际的定义要复杂一些,因为它包含了一些引力多态性的机制。如果你去看它的定义,实际上你会看到这个:
type role Code representational nominal
newtype Code m (a :: TYPE (r :: RuntimeRep)) = Code
{ examineCode :: m (TExp a) -- ^ Underlying monadic value
}
简化定义和实际定义之间的差异对于理解这篇文章并不重要,你可以在阅读这篇文章的时候记住简化定义。
值得庆幸的是,未类型化的TH接口基本没有变化,类型化的TH接口应该只需要使用Code ,而不是TExp ,就可以再次编译一些辅助函数了。
GHC 8代码示例
为了使这里的例子更容易理解,让我们定义一个小型的类型化Template Haskell模块,我们希望它能在GHC 8下工作,以后我们将把它移植到GHC 9。代码将非常简单:它的目的是将一个环境标志解析成一个已知的数据类型,或者在编译失败时完全停止编译。
我们将使用 template-haskell包,所以别忘了加载它。你还需要激活TemplateHaskell 语言扩展。
{-# LANGUAGE TemplateHaskell #-}
module TH where
import Control.Monad.IO.Class (liftIO)
import Language.Haskell.TH.Syntax (Q, TExp)
import System.Environment (lookupEnv)
data LogLevel
= Development -- ^ Log errors and debug messages
| Production -- ^ Only log errors
deriving (Show)
logLevelFromFlag :: Q (TExp LogLevel)
logLevelFromFlag = do
flag <- liftIO $ lookupEnv "LOG_LEVEL"
case flag of
Nothing -> [|| Production ||] -- We default to Production if the flag is unset
Just level -> case level of
"DEVELOPMENT" -> [|| Development ||]
"PRODUCTION" -> [|| Production ||]
other -> fail $ "Unrecognized LOG_LEVEL flag: " <> other
然而,如果你试图用GHC 9来编译这个模块,这个例子将无法编译。为了简洁起见,我们省略了类似的错误。
>>> :l TH
TH.hs:18:16: error:
• Couldn't match type ‘Code m0’ with ‘Q’
Expected: Q (TExp LogLevel)
Actual: Code m0 LogLevel
• In the Template Haskell quotation [|| Production ||]
In the expression: [|| Production ||]
In a case alternative: Nothing -> [|| Production ||]
|
18 | Nothing -> [|| Production ||] -- We default to Production if the flag is unset
| ^^^^^^^^^^^^^^^^^^
同样地,拼接也会要求新的代码返回Code ,这一点以一个洞为例。
>>> $$_
<interactive>:19:3: error:
• Found hole: _ :: Code Q p0
Where: ‘p0’ is an ambiguous type variable
• In the Template Haskell splice $$_
In the expression: $$_
In an equation for ‘it’: it = $$_
在下面的章节中,我们将看到如何将这段代码移植到GHC 9,而不与GHC 8向后兼容。
移植没有向后兼容的旧TTH代码
对于这一部分,你需要一个版本至少为9的GHC。请确保从template-haskell ,同时导入相应的定义。
-import Language.Haskell.TH.Syntax (Q, TExp)
+import Language.Haskell.TH.Syntax (Code, Q, examineCode, liftCode)
由于我们现在需要返回一个类型为Code 的值,我们改变了类型签名。现在我们可以通过使用两个新的函数,即examineCode 和liftCode 来修复代码。
-logLevelFromFlag :: Q (TExp LogLevel)
-logLevelFromFlag = do
+logLevelFromFlag :: Code Q LogLevel
+logLevelFromFlag = liftCode $ do -- liftCode will turn 'Q' into 'Code'
flag <- liftIO $ lookupEnv "LOG_LEVEL"
case flag of
+ -- examineCode will turn 'Code' into 'Q'
- Nothing -> [|| Production ||] -- We default to Production if the flag is unset
+ Nothing -> examineCode [|| Production ||] -- We default to Production if the flag is unset
Just level -> case level of
- "DEVELOPMENT" -> [|| Development ||]
- "PRODUCTION" -> [|| Production ||]
+ "DEVELOPMENT" -> examineCode [|| Development ||]
+ "PRODUCTION" -> examineCode [|| Production ||]
other -> fail $ "Unrecognized LOG_LEVEL flag: " <> other
上一节中发生的错误是因为引号所返回的类型不再是Q ,而是Code 。解决方案是使用examineCode ,将Code Q a 变成Q (TExp a) ,然后使用liftCode ,做相反的操作:将Q (TExp a) 变成Code Q a 。
liftCode :: forall (r :: RuntimeRep) (a :: TYPE r) m. m (TExp a) -> Code m a
examineCode :: forall (r :: RuntimeRep) (a :: TYPE r) m. Code m a -> m (TExp a)
就这样了!如果你想让这些代码在GHC 8上继续工作,请务必阅读下一节。
移植具有向后兼容性的旧TTH代码
对于本节,你可以选择使用GHC 8或GHC 9。我们将使用 th-compat包,除了熟悉的 template-haskell包,所以请确保你同时加载它们。
th-compat 是我们推荐的与TTH向后兼容的库。在这一节中,我们将展示如何将用法从 改为向后兼容的 ,它可以在GHC 9以及以前的版本上工作。Code Splice
这个模式与使用Code 的工作流程非常相似,尽管现在使用Splice 。
请确保首先导入适当的定义。
-import Language.Haskell.TH.Syntax (Code, Q, examineCode, liftCode)
+import Language.Haskell.TH.Syntax (Q)
+import Language.Haskell.TH.Syntax.Compat (Splice, examineSplice, liftSplice)
现在只要用Splice 替换Code 。
-logLevelFromFlag :: Code Q LogLevel
-logLevelFromFlag = liftCode $ do -- liftSplice will turn 'Q' into 'Code'
+logLevelFromFlag :: Splice Q LogLevel
+logLevelFromFlag = liftSplice $ do -- liftSplice will turn 'Q' into 'Splice'
flag <- liftIO $ lookupEnv "LOG_LEVEL"
case flag of
- -- examineCode will turn 'Code' into 'Q'
- Nothing -> examineCode [|| Production ||] -- We default to Production if the flag is unset
+ -- examineSplice will turn 'Splice' into 'Q'
+ Nothing -> examineSplice [|| Production ||] -- We default to Production if the flag is unset
Just level -> case level of
- "DEVELOPMENT" -> examineCode [|| Development ||]
- "PRODUCTION" -> examineCode [|| Production ||]
+ "DEVELOPMENT" -> examineSplice [|| Development ||]
+ "PRODUCTION" -> examineSplice [|| Production ||]
other -> fail $ "Unrecognized LOG_LEVEL flag: " <> other
th-compat 就这样!Splice m a 在GHC 9中被定义为Code m a ,在之前的版本中被定义为m (TExp a) ,这就是为什么这段代码可以工作。此外,像liftSplice 和examineSplice 这样的函数要么被定义为liftCode 和examineCode (在 GHC 9 中),要么被定义为id (在 GHC 8 及以下版本)。
拼接中的括号
在我们结束这篇文章之前,还有一件事。你可能会看到关于无类型和有类型的Template Haskell在拼接中使用括号的令人惊讶的行为。解析器在GHC 9中进行了调整,所以与之前的版本相比,在某些情况下,小括号是不必要的。
例如,如果我们导入了logLevelFromFlag 资格,那么在GHC 8中你会写$$(TH.logLevelFromFlag) ,否则你会得到一个解析器错误。
>>> $$TH.logLevelFromFlag
<interactive>:200:1: error: parse error on input ‘$$’
但在GHC 9中,它是有效的。
>>> $$TH.logLevelFromFlag
Production
结论
在这篇文章中,我们已经看到了GHC 9中关于类型化模板Haskell的变化。我们了解了将类型化的Template Haskell代码从GHC 8迁移到GHC 9的两种不同方式,并分析了它们的区别。
使用th-compat ,除了具有与GHC 9相似的接口外,还有一个很大的优势,那就是可以兼容各种GHC版本。另一方面,如果没有必要支持GHC 8,它就会成为你的项目中的一个额外的依赖,对许多用户来说可能是陌生的。选择使用哪种策略最终将取决于你的具体需求,但我们希望能阐明它们的优点和缺点。