Haskell:使准引用值严格/在编译时评估

Haskell: Making Quasi-Quoted values strict / evaluated at compile-time

我有一个'Month'类型,大致是

newtype Month = Month Word8

其中 Month 构造函数未导出;相反,一个函数

mon :: Word8 -> Maybe Month
mon i = if i > 0 && i < 13
        then Just $ Month i
        else Nothing

被导出,如果输入值介于 1 和 12 之间(含 1 和 12),它只会 return 一个值。

现在,使用 Language.Haskell.TH.Quote,我定义了一个准引用 ... 运算符? ...这让我可以 "create" 个月的实例 "at compile time":

month :: QuasiQuoter
month = QuasiQuoter { quoteDec  = error "quoteDec not implemented"
                    , quoteType = error "quoteType not implemented"
                    , quotePat  = "quotePat not implemented"
                    , quoteExp = (\ s → ⟦ force $ __fromString @Month s ⟧)
                    }


m :: Month
m = [month|3|]

其中 __fromString 解析字符串,并且 return 是一个值或调用 errorforce 来自 Control.DeepSeq.

现在这很好,但它的主要价值是尽早捕获错误值 - 但是,由于懒惰求值,值 m 也不会在编译时求值(这将是理想,但也许是一个相当高的要求)或至少在 运行 时间的最早阶段。

有什么方法可以注释该值(最好在下面的准引用中,这样每次使用 month 都可以免费获得它;但如果不行,注释 m)当程序获得 运行 时强制评估 m?需要 NFData 约束或类似约束即可。

谢谢,

你的准引用器只是将所有内容都放在引号中,从而将所有内容推迟到运行时。您需要将解析和验证移到引号之外。

我的概念快速证明:

{-# LANGUAGE TemplateHaskell, DeriveLift #-}
module A ( Month,
           mon,
           month
         ) where

import Text.Read
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift)
import Language.Haskell.TH.Quote

newtype Month = Month Int deriving (Show, Eq, Ord, Lift)

mon :: Int -> Maybe Month
mon n | n >= 1 && n <= 12 = Just $ Month n
      | otherwise = Nothing

monthExpImpl :: String -> Q Exp
monthExpImpl s = case readMaybe s of
  Nothing -> fail "Couldn't parse input as number"
  Just n -> case mon n of
    Nothing -> fail "Not a valid month"
    Just x -> [| x |]

month :: QuasiQuoter
month = QuasiQuoter { quoteDec  = error "quoteDec not implemented"
                    , quoteType = error "quoteType not implemented"
                    , quotePat  = error "quotePat not implemented"
                    , quoteExp = monthExpImpl
                    }

请注意,monthExpImpl 将所有逻辑放在引号之外。 fail 是终止带有编译错误的 Q 操作的推荐方法,对于习惯于将 fail 视为我们正在远离的历史事故的人来说,这感觉很奇怪。

这里最令人惊讶的部分是 DeriveLift 扩展及其将 Lift 添加到 Month 的派生 类 列表中的用途。 TH 使用 Lift 将值转换为生成该值的代码。没有它,编译器不知道如何将 [| x |] 引用变成代码。

您可能想知道 TH 生成调用构造函数的代码的有效性如何,该构造函数不应从生成代码所在的编译单元中可见。我也想知道。原来还好,只要在TH中创建构造函数的代码就可以看到构造函数。在本例中,是 Lift 实例在执行此操作,并且它是在同一模块中定义的,因此它可以看到构造函数。这可能会让您在创建此类实例时犹豫不决,因为您无法阻止实例被导出。这是一个有效的考虑。不过,在这种情况下没问题,因为 lift 需要一个值才能转换为代码,而从模块外部获取此类值的唯一*方法无论如何都是通过 mon,所以它不会介绍任何新的方法来搞砸事情。 (我说 "only*" 因为 unsafeCoerce 存在,但我们就假装它不存在。当你使用它时,无论如何你都必须承担破坏一切的责任。)