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 是一个值或调用 error
。 force
来自 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
存在,但我们就假装它不存在。当你使用它时,无论如何你都必须承担破坏一切的责任。)
我有一个'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 是一个值或调用 error
。 force
来自 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
存在,但我们就假装它不存在。当你使用它时,无论如何你都必须承担破坏一切的责任。)