5

我有一个“月”类型,大致是

newtype Month = Month Word8

Month没有导出构造函数的地方;相反,一个函数

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

被导出,仅当输入值介于 1 和 12 之间时才会返回一个值。

现在,使用Language.Haskell.TH.Quote,我定义了一个准引用 ... 运算符?...这允许我“在编译时”“创建” Month 的实例:

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|]

Where__fromString解析一个字符串,并返回一个值或调用error. force是从Control.DeepSeq

现在这很好,但它的主要价值是尽早捕获坏值 - 但是,由于延迟评估,值 m 也不会在编译时评估(这将是理想的,而是也许是高阶)或至少在运行时的最早阶段。

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

谢谢,

4

1 回答 1

6

您的准报价者只是通过将所有内容放在引号中来将所有内容推迟到运行时。您需要将解析和验证移到引用之外。

我的概念快速证明:

{-# 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扩展及其用于添加LiftMonth. LiftTH 使用它来将值转换为生成该值的代码。没有它,编译器不知道如何将[| x |]引号变成代码。

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

于 2019-12-19T16:33:00.427 回答